[
  {
    "path": ".github/workflows/continuous-deployment-dev-new.yml",
    "content": "name: AOE Continuous Deployment (DEV NEW)\non: \n  workflow_dispatch:\n  push:\n    branches:\n      - dev\npermissions:\n      id-token: write\n      contents: read      \njobs:\n  AOE-CD:\n    environment: devnew\n    runs-on: ubuntu-latest\n    env:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }}\n      AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }}\n      AOE_LOCATION: ${{ secrets.AOE_LOCATION }}\n      AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }}\n    steps:\n      - run: echo \"This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!\"\n      - name: Installing modules\n        shell: pwsh\n        run: |\n          Set-PSRepository PSGallery -InstallationPolicy Trusted\n          Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force\n      - name: Check out repository code\n        uses: actions/checkout@v3\n      - name: Login via Az module\n        uses: azure/login@hf_447_release\n        with:\n          client-id: ${{ secrets.AZURE_CLIENT_ID }}\n          tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          enable-AzPSSession: true \n      - name: Create Deployment Settings JSON file\n        run: |\n          echo '{\n            \"SubscriptionId\": \"'\"$AZURE_SUBSCRIPTION_ID\"'\",\n            \"NamePrefix\": \"'\"$AOE_NAMEPREFIX$(date '+%Y%m%d%H')\"'\",\n            \"WorkspaceReuse\": \"n\",\n            \"DeployWorkbooks\": \"y\",\n            \"SqlAdmin\": \"'\"$AOE_SQL_ADMIN\"'\",\n            \"SqlPass\": \"'\"$AOE_SQL_PASSWD\"'\",\n            \"TargetLocation\": \"'\"$AOE_LOCATION\"'\",\n            \"DeployBenefitsUsageDependencies\": \"n\"\n          }' > ./deploymentSettings.json          \n      - name: Testing PowerShell script call\n        shell: pwsh\n        run: |\n          ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri \"https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/dev/azuredeploy.bicep\"\n      - run: echo \"🍏 This job's status is ${{ job.status }}.\"\n"
  },
  {
    "path": ".github/workflows/continuous-deployment-dev.yml",
    "content": "name: AOE Continuous Deployment (DEV)\non: \n  workflow_dispatch:\n  push:\n    branches:\n      - dev\npermissions:\n      id-token: write\n      contents: read      \njobs:\n  AOE-CD:\n    environment: dev\n    runs-on: ubuntu-latest\n    env:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }}\n      AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }}\n      AOE_LOCATION: ${{ secrets.AOE_LOCATION }}\n      AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }}\n      AOE_WORKSPACENAME: ${{ secrets.AOE_WORKSPACENAME }}\n      AOE_WORKSPACERG: ${{ secrets.AOE_WORKSPACERG }}\n    steps:\n      - run: echo \"This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!\"\n      - name: Installing modules\n        shell: pwsh\n        run: |\n          Set-PSRepository PSGallery -InstallationPolicy Trusted\n          Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force\n      - name: Check out repository code\n        uses: actions/checkout@v3\n      - name: Login via Az module\n        uses: azure/login@hf_447_release\n        with:\n          client-id: ${{ secrets.AZURE_CLIENT_ID }}\n          tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          enable-AzPSSession: true \n      - name: Create Deployment Settings JSON file\n        run: |\n          echo '{\n            \"SubscriptionId\": \"'\"$AZURE_SUBSCRIPTION_ID\"'\",\n            \"NamePrefix\": \"'\"$AOE_NAMEPREFIX\"'\",\n            \"WorkspaceReuse\": \"y\",\n            \"WorkspaceName\": \"'\"$AOE_WORKSPACENAME\"'\",\n            \"WorkspaceResourceGroupName\": \"'\"$AOE_WORKSPACERG\"'\",\n            \"DeployWorkbooks\": \"y\",\n            \"SqlAdmin\": \"'\"$AOE_SQL_ADMIN\"'\",\n            \"SqlPass\": \"'\"$AOE_SQL_PASSWD\"'\",\n            \"TargetLocation\": \"'\"$AOE_LOCATION\"'\",\n            \"DeployBenefitsUsageDependencies\": \"n\"\n          }' > ./deploymentSettings.json          \n      - name: Testing PowerShell script call\n        shell: pwsh\n        run: |\n          ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json -TemplateUri \"https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/dev/azuredeploy.bicep\" -DoPartialUpgrade\n      - run: echo \"🍏 This job's status is ${{ job.status }}.\"\n"
  },
  {
    "path": ".github/workflows/continuous-deployment.yml",
    "content": "name: AOE Continuous Deployment (PROD)\non: \n  workflow_dispatch:\n  push:\n    branches:\n      - master\npermissions:\n      id-token: write\n      contents: read      \njobs:\n  AOE-CD:\n    environment: prod\n    runs-on: ubuntu-latest\n    env:\n      AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n      AOE_SQL_ADMIN: ${{ secrets.AOE_SQL_ADMIN }}\n      AOE_SQL_PASSWD: ${{ secrets.AOE_SQL_PASSWD }}\n      AOE_LOCATION: ${{ secrets.AOE_LOCATION }}\n      AOE_NAMEPREFIX: ${{ secrets.AOE_NAMEPREFIX }}\n    steps:\n      - run: echo \"This job is now running on a ${{ runner.os }} server hosted by GitHub for the ${{ github.ref }} branch of the ${{ github.repository }} repository!\"\n      - name: Installing modules\n        shell: pwsh\n        run: |\n          Set-PSRepository PSGallery -InstallationPolicy Trusted\n          Install-Module -Name Az.Accounts,Az.Resources,Az.Storage,Az.OperationalInsights,Az.Sql,Az.Automation,Microsoft.Graph.Authentication,Microsoft.Graph.Identity.DirectoryManagement -Force\n      - name: Check out repository code\n        uses: actions/checkout@v3\n      - name: Login via Az module\n        uses: azure/login@hf_447_release\n        with:\n          client-id: ${{ secrets.AZURE_CLIENT_ID }}\n          tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n          enable-AzPSSession: true \n      - name: Create Deployment Settings JSON file\n        run: |\n          echo '{\n            \"SubscriptionId\": \"'\"$AZURE_SUBSCRIPTION_ID\"'\",\n            \"NamePrefix\": \"'\"$AOE_NAMEPREFIX\"'\",\n            \"WorkspaceReuse\": \"n\",\n            \"DeployWorkbooks\": \"y\",\n            \"SqlAdmin\": \"'\"$AOE_SQL_ADMIN\"'\",\n            \"SqlPass\": \"'\"$AOE_SQL_PASSWD\"'\",\n            \"TargetLocation\": \"'\"$AOE_LOCATION\"'\",\n            \"DeployBenefitsUsageDependencies\": \"n\"\n          }' > ./deploymentSettings.json          \n      - name: Testing PowerShell script call\n        shell: pwsh\n        run: |\n          ./Deploy-AzureOptimizationEngine.ps1 -SilentDeploymentSettingsPath ./deploymentSettings.json\n      - run: echo \"🍏 This job's status is ${{ job.status }}.\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# Deployment state file\nlast-deployment-state.json\n# Database connection settings (for the Suppress-Recommendation.ps1 helper script)\ndatabase-connection-settings.json\n# Silent deployment settings file\ndeployment-settings-*.json"
  },
  {
    "path": "Deploy-AzureOptimizationEngine.ps1",
    "content": "param (\n    [Parameter(Mandatory = $false)]\n    [string] $TemplateUri,\n\n    [Parameter(Mandatory = $false)]\n    [string] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $false)]\n    [switch] $DoPartialUpgrade,\n\n    [Parameter(Mandatory = $false)]\n    [switch] $IgnoreNamingAvailabilityErrors,\n\n    [Parameter(Mandatory = $false)]\n    [string] $SilentDeploymentSettingsPath,\n\n    [Parameter(Mandatory = $false)]\n    [hashtable] $ResourceTags = @{}\n)\n\nfunction ConvertTo-Hashtable {\n    [CmdletBinding()]\n    [OutputType('hashtable')]\n    param (\n        [Parameter(ValueFromPipeline)]\n        $InputObject\n    )\n \n    process {\n        if ($null -eq $InputObject) {\n            return $null\n        }\n \n        if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {\n            $collection = @(\n                foreach ($object in $InputObject) {\n                    ConvertTo-Hashtable -InputObject $object\n                }\n            ) \n            Write-Output -NoEnumerate $collection\n        } elseif ($InputObject -is [psobject]) { \n            $hash = @{}\n            foreach ($property in $InputObject.PSObject.Properties) {\n                $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value\n            }\n            $hash\n        } else {\n            $InputObject\n        }\n    }\n}\n\nfunction Test-SqlPasswordComplexity {\n    param (\n        [string]$Username,    \n        [string]$Password\n    )\n\n    # Check if the username is present in the password\n    if ($Password -match $Username) {\n        throw \"SQL password cannot contain the SQL username.\"\n        return $false\n    }\n\n    # Password must be minimum 8 characters, contains at least one uppercase, lowercase letter, contains at least one digit, contains at least one special character\n    $regex = '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^\\da-zA-Z]).{8,}$'\n    if ($Password -match $regex) {\n        Write-Host \"SQL password is valid.\" -ForegroundColor Green\n        return $true\n    } else {\n        throw \"Password does not meet the complexity requirements.\"\n        return $false\n    }\n}\n\n$ErrorActionPreference = \"Stop\"\n\n#region Deployment environment settings\n\n$lastDeploymentStatePath = \".\\last-deployment-state.json\"\n$deploymentOptions = @{}\n$silentDeploy = $false\n\n# Check if silent deployment settings file exists\nif(-not([string]::IsNullOrEmpty($SilentDeploymentSettingsPath)) -and (Test-Path -Path $SilentDeploymentSettingsPath))\n{\n    $silentDeploy = $true\n    # Get the deployment details from the silent deployment settings file\n    $silentDepOptions = Get-Content -Path $SilentDeploymentSettingsPath | ConvertFrom-Json\n    Write-Host \"Silent deployment options found.\" -ForegroundColor Green\n    $silentDepOptions = ConvertTo-Hashtable -InputObject $silentDepOptions\n    $silentDepOptions.Keys | ForEach-Object {\n        $deploymentOptions[$_] = $silentDepOptions[$_]\n    }\n\n    # Validate the silent deployment settings\n    if (-not($deploymentOptions[\"SubscriptionId\"]))\n    {\n        throw \"SubscriptionId is required for silent deployment.\"\n    }\n    if (-not($deploymentOptions[\"NamePrefix\"]))\n    {\n        throw \"NamePrefix is required for silent deployment. Set to 'EmptyNamePrefix' to use own naming convention and specify the needed resource names.\"\n    }\n    if ($deploymentOptions[\"NamePrefix\"].Length -gt 21) {\n        throw \"Name prefix length is larger than the 21 characters limit ($($deploymentOptions[\"NamePrefix\"]))\"\n    }\n    if ($deploymentOptions[\"NamePrefix\"] -eq \"EmptyNamePrefix\")\n    {\n        if (-not($deploymentOptions[\"ResourceGroupName\"]))\n        {\n            throw \"ResourceGroupName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'.\"\n        }\n        if (-not($deploymentOptions[\"StorageAccountName\"]))\n        {\n            throw \"StorageAccountName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'.\"\n        }\n        if (-not($deploymentOptions[\"AutomationAccountName\"]))\n        {\n            throw \"AutomationAccountName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'.\"\n        }\n        if (-not($deploymentOptions[\"SqlServerName\"]))\n        {\n            throw \"SqlServerName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'.\"\n        }\n        if (-not($deploymentOptions[\"SqlDatabaseName\"]))\n        {\n            throw \"SqlDatabaseName is required for silent deployment when NamePrefix is set to 'EmptyNamePrefix'.\"\n        }\n    }\n    if (-not($deploymentOptions[\"WorkspaceReuse\"]) -or ($deploymentOptions[\"WorkspaceReuse\"] -ne \"y\" -and $deploymentOptions[\"WorkspaceReuse\"] -ne \"n\"))\n    {\n        throw \"WorkspaceReuse set to 'y' or 'n' is required for silent deployment.\"\n    }\n    if ($deploymentOptions[\"WorkspaceReuse\"] -eq \"y\")\n    {\n        if (-not($deploymentOptions[\"WorkspaceName\"]))\n        {\n            throw \"WorkspaceName is required for silent deployment when WorkspaceReuse is set to 'y'.\"\n        }\n        if (-not($deploymentOptions[\"WorkspaceResourceGroupName\"]))\n        {\n            throw \"WorkspaceResourceGroupName is required for silent deployment when WorkspaceReuse is set to 'y'.\"\n        }\n    }\n    if (-not($deploymentOptions[\"DeployWorkbooks\"]) -or ($deploymentOptions[\"DeployWorkbooks\"] -ne \"y\" -and $deploymentOptions[\"DeployWorkbooks\"] -ne \"n\"))\n    {\n        throw \"DeployWorkbooks set to 'y' or 'n' is required for silent deployment.\"\n    }\n    if (-not($deploymentOptions[\"SqlAdmin\"]))\n    {\n        throw \"SqlAdmin is required for silent deployment.\"\n    }\n    if (-not($deploymentOptions[\"SqlPass\"]))\n    {\n        throw \"SqlPass is required for silent deployment.\"\n    }\n    if (-not($deploymentOptions[\"TargetLocation\"]))\n    {\n        throw \"TargetLocation is required for silent deployment.\"\n    }\n    if (-not($deploymentOptions[\"DeployBenefitsUsageDependencies\"]))\n    {\n        throw \"DeployBenefitsUsageDependencies is required for silent deployment.\"\n    }\n    if ($deploymentOptions[\"DeployBenefitsUsageDependencies\"] -eq \"y\")\n    {\n        if (-not($deploymentOptions[\"CustomerType\"]))\n        {\n            throw \"CustomerType is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'.\"\n        }\n        if (-not($deploymentOptions[\"BillingAccountId\"]))\n        {\n            throw \"BillingAccountId is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'.\"\n        }\n        if (-not($deploymentOptions[\"CurrencyCode\"]))\n        {\n            throw \"CurrencyCode is required for silent deployment when DeployBenefitsUsageDependencies is set to 'y'.\"\n        }\n        if ($deploymentOptions[\"CustomerType\"] -eq \"MCA\")\n        {\n            if (-not($deploymentOptions[\"BillingProfileId\"]))\n            {\n                throw \"BillingProfileId is required for silent deployment when CustomerType is set to 'MCA'.\"\n            }\n        }\n    }\n}\n\nif ((Test-Path -Path $lastDeploymentStatePath) -and !$silentDeploy)\n{\n    $depOptions = Get-Content -Path $lastDeploymentStatePath | ConvertFrom-Json\n    Write-Host $depOptions -ForegroundColor Green\n    $depOptionsReuse = Read-Host \"Found last deployment options above. Do you want to repeat/upgrade last deployment (Y/N)?\"\n    if (\"Y\", \"y\" -contains $depOptionsReuse)\n    {\n        foreach ($property in $depOptions.PSObject.Properties)\n        {\n            $deploymentOptions[$property.Name] = $property.Value\n        }    \n    }\n}\n\n$GitHubOriginalUri = \"https://raw.githubusercontent.com/helderpinto/AzureOptimizationEngine/master/azuredeploy.bicep\"\n\nif ([string]::IsNullOrEmpty($TemplateUri)) {\n    $TemplateUri = $GitHubOriginalUri\n}\n\n$isTemplateAvailable = $false\n\ntry {\n    Invoke-WebRequest -Uri $TemplateUri | Out-Null\n    $isTemplateAvailable = $true\n}\ncatch {\n    Write-Host \"The template URL ($TemplateUri) is not available. Please, put it in a publicly accessible HTTPS location.\" -ForegroundColor Red\n}\n\nif (!$isTemplateAvailable) {\n    throw \"Terminating due to template unavailability.\"\n}\n\nif (-not((Test-Path -Path \"./azuredeploy.bicep\") -and (Test-Path -Path \"./azuredeploy-nested.bicep\"))) {\n    throw \"Terminating due to template unavailability. Please, change directory to where azuredeploy.bicep and azuredeploy-nested.bicep are located.\"\n}\n\n$cloudDetails = Get-AzEnvironment -Name $AzureEnvironment\n\n$ctx = Get-AzContext\nif (-not($ctx)) {\n    Connect-AzAccount -Environment $AzureEnvironment\n    $ctx = Get-AzContext\n}\nelse {\n    if ($ctx.Environment.Name -ne $AzureEnvironment) {\n        Disconnect-AzAccount -ContextName $ctx.Name\n        Connect-AzAccount -Environment $AzureEnvironment\n        $ctx = Get-AzContext\n    }\n}\n\n#endregion\n\n#region Azure subscription choice\n\nWrite-Host \"Getting Azure subscriptions...\" -ForegroundColor Yellow\n$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" -and $_.SubscriptionPolicies.QuotaId -notlike \"AAD*\" }\n\nif ($subscriptions.Count -gt 1) {\n\n    $selectedSubscription = -1\n    for ($i = 0; $i -lt $subscriptions.Count; $i++)\n    {\n        if (-not($deploymentOptions[\"SubscriptionId\"]))\n        {\n            Write-Output \"[$i] $($subscriptions[$i].Name)\"    \n        }\n        else\n        {\n            if ($subscriptions[$i].Id -eq $deploymentOptions[\"SubscriptionId\"])\n            {\n                $selectedSubscription = $i\n                break\n            }\n        }\n    }\n    if (-not($deploymentOptions[\"SubscriptionId\"]))\n    {\n        $lastSubscriptionIndex = $subscriptions.Count - 1\n        while ($selectedSubscription -lt 0 -or $selectedSubscription -gt $lastSubscriptionIndex) {\n            Write-Output \"---\"\n            $selectedSubscription = [int] (Read-Host \"Please, select the target subscription for this deployment [0..$lastSubscriptionIndex]\")\n        }    \n    }\n    if ($selectedSubscription -eq -1)\n    {\n        throw \"The selected subscription does not exist. Check if you are logged in with the right Microsoft Entra ID user.\"        \n    }\n}\nelse\n{\n    if ($subscriptions.Count -ne 0)\n    {\n        $selectedSubscription = 0\n    }\n    else\n    {\n        throw \"No valid subscriptions found. Only EA, MCA, PAYG or MSDN subscriptions are supported currently.\"\n    }\n}\n\nif ($subscriptions.Count -eq 0) {\n    throw \"No subscriptions found. Check if you are logged in with the right Microsoft Entra ID account.\"\n}\n\n$subscriptionId = $subscriptions[$selectedSubscription].Id\n\nif (-not($deploymentOptions[\"SubscriptionId\"]))\n{\n    $deploymentOptions[\"SubscriptionId\"] = $subscriptionId\n}\n\nif ($ctx.Subscription.Id -ne $subscriptionId) {\n    $ctx = Select-AzSubscription -SubscriptionId $subscriptionId\n}\n\n#endregion\n\n#region Resource naming options\nif($silentDeploy)\n{\n    $workspaceReuse = $deploymentOptions[\"WorkspaceReuse\"]\n}\nelse { \n    $workspaceReuse = $null \n}\n\n$deploymentNameTemplate = \"{0}\" + (Get-Date).ToString(\"yyMMddHHmmss\")\n$resourceGroupNameTemplate = \"{0}-rg\"\n$storageAccountNameTemplate = \"{0}sa\"\n$laWorkspaceNameTemplate = \"{0}-la\"\n$automationAccountNameTemplate = \"{0}-auto\"\n$sqlServerNameTemplate = \"{0}-sql\"\n\n$nameAvailable = $true\nif (-not($deploymentOptions[\"NamePrefix\"]))\n{\n    do\n    {\n        $namePrefix = Read-Host \"Please, enter a unique name prefix for the deployment (max. 21 chars) or existing prefix if updating deployment. If you want instead to individually name all resources, just press ENTER\"\n        if (-not($namePrefix))\n        {\n            $namePrefix = \"EmptyNamePrefix\"\n        }\n    } \n    while ($namePrefix.Length -gt 21)\n    $deploymentOptions[\"NamePrefix\"] = $namePrefix\n}\nelse {\n    if ($deploymentOptions[\"NamePrefix\"] -eq \"EmptyNamePrefix\")\n    {\n        $namePrefix = $null\n    }\n    else\n    {\n        $namePrefix = $deploymentOptions[\"NamePrefix\"]            \n    }\n}\n\nif (-not($deploymentOptions[\"WorkspaceReuse\"]))\n{\n    if ($null -eq $workspaceReuse) {\n        $workspaceReuse = Read-Host \"Are you going to reuse an existing Log Analytics workspace (Y/N)?\"\n    }\n    $deploymentOptions[\"WorkspaceReuse\"] = $workspaceReuse\n}\nelse\n{\n    $workspaceReuse = $deploymentOptions[\"WorkspaceReuse\"]\n}\n\nif (-not($deploymentOptions[\"ResourceGroupName\"]))\n{\n    if ([string]::IsNullOrEmpty($namePrefix) -or $namePrefix -eq \"EmptyNamePrefix\") {\n        $resourceGroupName = Read-Host \"Please, enter the new or existing Resource Group for this deployment\"\n        $deploymentName = $deploymentNameTemplate -f $resourceGroupName\n        $storageAccountName = Read-Host \"Enter the Storage Account name\"\n        $automationAccountName = Read-Host \"Automation Account name\"\n        $sqlServerName = Read-Host \"Azure SQL Server name\"\n        $sqlDatabaseName = Read-Host \"Azure SQL Database name\"\n        if (\"N\", \"n\" -contains $workspaceReuse) {\n            $laWorkspaceName = Read-Host \"Log Analytics Workspace\"\n        }\n    }\n    else {\n        $deploymentName = $deploymentNameTemplate -f $namePrefix\n        $resourceGroupName = $resourceGroupNameTemplate -f $namePrefix\n        $storageAccountName = $storageAccountNameTemplate -f $namePrefix\n        $automationAccountName = $automationAccountNameTemplate -f $namePrefix\n        $sqlServerName = $sqlServerNameTemplate -f $namePrefix                    \n        if (\"Y\", \"y\" -contains $workspaceReuse -and $silentDeploy) {\n            $laWorkspaceName = $deploymentOptions[\"WorkspaceName\"]\n        }\n        else {\n            $laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix\n        }\n        $sqlDatabaseName = \"azureoptimization\"\n    }\n\n    $deploymentOptions[\"ResourceGroupName\"] = $resourceGroupName\n    $deploymentOptions[\"StorageAccountName\"] = $storageAccountName\n    $deploymentOptions[\"AutomationAccountName\"] = $automationAccountName\n    $deploymentOptions[\"SqlServerName\"] = $sqlServerName\n    $deploymentOptions[\"SqlDatabaseName\"] = $sqlDatabaseName\n    $deploymentOptions[\"WorkspaceName\"] = $laWorkspaceName\n}\nelse\n{\n    # With a silent deploy, overrule any custom resource naming if a NamePrefix is provided\n    if($silentDeploy -and ![string]::IsNullOrEmpty($namePrefix) -and $namePrefix -ne \"EmptyNamePrefix\")\n    {\n        $deploymentName = $deploymentNameTemplate -f $namePrefix\n        $resourceGroupName = $resourceGroupNameTemplate -f $namePrefix\n        $storageAccountName = $storageAccountNameTemplate -f $namePrefix\n        $automationAccountName = $automationAccountNameTemplate -f $namePrefix\n        $sqlServerName = $sqlServerNameTemplate -f $namePrefix\n        if (\"Y\", \"y\" -contains $workspaceReuse) {\n            $laWorkspaceName = $deploymentOptions[\"WorkspaceName\"]\n        }\n        else {\n            $laWorkspaceName = $laWorkspaceNameTemplate -f $namePrefix\n        }\n        $sqlDatabaseName = \"azureoptimization\"\n    }\n    else {\n        $resourceGroupName = $deploymentOptions[\"ResourceGroupName\"]\n        $storageAccountName = $deploymentOptions[\"StorageAccountName\"]\n        $automationAccountName = $deploymentOptions[\"AutomationAccountName\"]\n        $sqlServerName = $deploymentOptions[\"SqlServerName\"]\n        $sqlDatabaseName = $deploymentOptions[\"SqlDatabaseName\"]        \n        $laWorkspaceName = $deploymentOptions[\"WorkspaceName\"]        \n        $deploymentName = $deploymentNameTemplate -f $resourceGroupName\n    }\n}\n#endregion\n\n#region Resource naming availability checks\nWrite-Host \"Checking name prefix availability...\" -ForegroundColor Green\n\nWrite-Host \"...for the Storage Account...\" -ForegroundColor Green\n$sa = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName -ErrorAction SilentlyContinue\nif ($null -eq $sa) {\n    $saNameResult = Get-AzStorageAccountNameAvailability -Name $storageAccountName\n    if (-not($saNameResult.NameAvailable)) {\n        $nameAvailable = $false\n        Write-Host \"$($saNameResult.Message)\" -ForegroundColor Red\n    }    \n}\nelse {\n    Write-Host \"(The Storage Account was already deployed)\" -ForegroundColor Green\n}\n\nif (\"N\", \"n\" -contains $workspaceReuse) {\n    Write-Host \"...for the Log Analytics workspace...\" -ForegroundColor Green\n\n    $logAnalyticsReuse = $false\n    $laWorkspaceResourceGroup = $resourceGroupName\n\n    $la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName -Name $laWorkspaceName -ErrorAction SilentlyContinue\n    if ($null -eq $la) {\n        $laNameResult = Invoke-WebRequest -Uri \"https://portal.loganalytics.io/api/workspaces/IsWorkspaceExists?name=$laWorkspaceName\"\n        if ($laNameResult.Content -eq \"true\") {\n            $nameAvailable = $false\n            Write-Host \"The Log Analytics workspace $laWorkspaceName is already taken.\" -ForegroundColor Red\n        }\n    }\n    else {\n        Write-Host \"(The Log Analytics Workspace was already deployed)\" -ForegroundColor Green\n    }\n}\nelse {\n    $logAnalyticsReuse = $true\n}\n\nWrite-Host \"...for the Azure SQL Server...\" -ForegroundColor Green\n$sql = Get-AzSqlServer -ResourceGroupName $resourceGroupName -Name $sqlServerName -ErrorAction SilentlyContinue\nif ($null -eq $sql -and -not($sqlServerName -like \"*.database.*\") -and -not($IgnoreNamingAvailabilityErrors)) {\n\n    $SqlServerNameAvailabilityUriPath = \"/subscriptions/$subscriptionId/providers/Microsoft.Sql/checkNameAvailability?api-version=2014-04-01\"\n    $body = \"{`\"name`\": `\"$sqlServerName`\", `\"type`\": `\"Microsoft.Sql/servers`\"}\"\n    $sqlNameResult = (Invoke-AzRestMethod -Path $SqlServerNameAvailabilityUriPath -Method POST -Payload $body).Content | ConvertFrom-Json\n    \n    if (-not($sqlNameResult.available)) {\n        $nameAvailable = $false\n        Write-Host \"$($sqlNameResult.message) ($sqlServerName)\" -ForegroundColor Red\n    }\n}\nelse {\n    Write-Host \"(The SQL Server was already deployed)\" -ForegroundColor Green\n}\n\nif (-not($nameAvailable) -and -not($IgnoreNamingAvailabilityErrors))\n{\n    throw \"Please, fix naming issues. Terminating execution.\"\n}\n\nWrite-Host \"Chosen resource names are available for all services\" -ForegroundColor Green\n#endregion\n\n#region Additional resource options (LA reused, region, SQL user)\nif (-not($deploymentOptions[\"WorkspaceResourceGroupName\"]))\n{\n    if (\"Y\", \"y\" -contains $workspaceReuse) {\n        $laWorkspaceName = Read-Host \"Please, enter the name of the Log Analytics workspace to be reused\"\n        $laWorkspaceResourceGroup = Read-Host \"Please, enter the name of the resource group containing Log Analytics $laWorkspaceName\"\n        $la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $laWorkspaceResourceGroup -Name $laWorkspaceName -ErrorAction SilentlyContinue\n        if (-not($la)) {\n            throw \"Could not find $laWorkspaceName in resource group $laWorkspaceResourceGroup for the chosen subscription. Aborting.\"\n        }        \n        $deploymentOptions[\"WorkspaceName\"] = $laWorkspaceName\n        $deploymentOptions[\"WorkspaceResourceGroupName\"] = $laWorkspaceResourceGroup\n    }    \n}\nelse\n{\n    $laWorkspaceResourceGroup = $deploymentOptions[\"WorkspaceResourceGroupName\"]\n}\n\n$rg = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue \n\nif (-not($deploymentOptions[\"TargetLocation\"]))\n{\n    if (-not($rg.Location)) {\n        Write-Host \"Getting Azure locations...\" -ForegroundColor Green\n        $locations = Get-AzLocation | Where-Object { $_.Providers -contains \"Microsoft.Automation\" -and $_.Providers -contains \"Microsoft.Sql\" `\n                                                        -and $_.Providers -contains \"Microsoft.OperationalInsights\" `\n                                                        -and $_.Providers -contains \"Microsoft.Storage\"} | Sort-Object -Property Location\n        \n        for ($i = 0; $i -lt $locations.Count; $i++) {\n            Write-Output \"[$i] $($locations[$i].location)\"    \n        }\n        $selectedLocation = -1\n        $lastLocationIndex = $locations.Count - 1\n        while ($selectedLocation -lt 0 -or $selectedLocation -gt $lastLocationIndex) {\n            Write-Output \"---\"\n            $selectedLocation = [int] (Read-Host \"Please, select the target location for this deployment [0..$lastLocationIndex]\")\n        }\n        \n        $targetLocation = $locations[$selectedLocation].location    \n    }\n    else {\n        $targetLocation = $rg.Location    \n    }\n    \n    $deploymentOptions[\"TargetLocation\"] = $targetLocation\n}\nelse\n{\n    $targetLocation = $deploymentOptions[\"TargetLocation\"]    \n}\n\nif (-not($deploymentOptions[\"SqlAdmin\"]))\n{\n    $sqlAdmin = Read-Host \"Please, input the SQL Admin username\"\n    $deploymentOptions[\"SqlAdmin\"] = $sqlAdmin\n}\nelse\n{\n    $sqlAdmin = $deploymentOptions[\"SqlAdmin\"]    \n}\nif (-not($deploymentOptions[\"SqlPass\"]))\n{\n    $sqlPass = Read-Host \"Please, input the SQL Admin ($sqlAdmin) password\" -AsSecureString\n}\nelse\n{\n    $sqlPass = $deploymentOptions[\"SqlPass\"]\n    if(Test-SqlPasswordComplexity -Username $sqlAdmin -Password $sqlPass -ErrorAction SilentlyContinue)\n    {\n        Write-Host \"Password complexity check passed\" -ForegroundColor Green\n        $sqlPass = ConvertTo-SecureString -AsPlainText $sqlPass -Force\n    }\n    else\n    {\n        throw \"SQL password complexity check failed. Please, fix the password and try again.\"\n    }\n}\n#endregion\n\n#region Partial upgrade dependent resource checks\nif (-not($DoPartialUpgrade))\n{\n    $upgrading = $false\n}\nelse\n{\n    $upgrading = $true\n\n    if ($null -ne $rg)\n    {\n        if ($upgrading -and $null -ne $sa) \n        {\n            $containers = Get-AzStorageContainer -Context $sa.Context\n        }\n        else\n        {\n            $upgrading = $false    \n            Write-Host \"Did not find the $storageAccountName Storage Account.\" -ForegroundColor Yellow\n        }\n    \n        if ($upgrading -and $null -ne $sql)\n        {\n            $databases = Get-AzSqlDatabase -ServerName $sql.ServerName -ResourceGroupName $resourceGroupName\n            if (-not($databases | Where-Object { $_.DatabaseName -eq $sqlDatabaseName}))\n            {\n                $upgrading = $false\n                Write-Host \"Did not find the $sqlDatabaseName database.\" -ForegroundColor Yellow\n            }\n        }\n        else\n        {\n            if (-not($IgnoreNamingAvailabilityErrors))\n            {\n                $upgrading = $false    \n                Write-Host \"Did not find the $sqlServerName SQL Server.\" -ForegroundColor Yellow    \n            }\n        }\n    \n        $auto = Get-AzAutomationAccount -ResourceGroupName $resourceGroupName -Name $automationAccountName -ErrorAction SilentlyContinue\n        if ($null -ne $auto)\n        {\n            $runbooks = Get-AzAutomationRunbook -ResourceGroupName $resourceGroupName `\n                -AutomationAccountName $auto.AutomationAccountName | Where-Object { $_.Name.StartsWith('Export') }\n            if ($runbooks.Count -lt 3)\n            {\n                $upgrading = $false    \n                Write-Host \"Did not find existing runbooks in the $automationAccountName Automation Account.\" -ForegroundColor Yellow\n            }\n        }\n        else\n        {\n            $upgrading = $false    \n            Write-Host \"Did not find the $automationAccountName Automation Account.\" -ForegroundColor Yellow\n        }\n    }\n    else\n    {\n        $upgrading = $false    \n    }        \n}\n#endregion\n\n$deploymentMessage = \"Deploying Azure Optimization Engine to subscription\"\nif ($upgrading)\n{\n    Write-Host \"Looks like this deployment was already done in the past. We will only upgrade runbooks, modules, schedules, variables, storage and the database.\" -ForegroundColor Yellow\n    $deploymentMessage = \"Upgrading Azure Optimization Engine in subscription\"\n}\n\nif ($silentDeploy)\n{\n    $continueInput = \"Y\"\n}\nelse\n{\n    $continueInput = Read-Host \"$deploymentMessage $($subscriptions[$selectedSubscription].Name). Continue (Y/N)?\"\n}\nif (\"Y\", \"y\" -contains $continueInput) {\n\n    # If we deploy silently, be sure to strip the SQL password from the output\n    if ($silentDeploy)\n    {\n        $deploymentOptions.Remove(\"SqlPass\")\n    }\n    $deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force\n    #region Computing schedules base time\n    $baseTime = (Get-Date).ToUniversalTime().ToString(\"u\")\n    $upgradingSchedules = $false\n    $schedules = Get-AzAutomationSchedule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -ErrorAction SilentlyContinue\n    if ($schedules.Count -gt 0) {\n        $upgradingSchedules = $true\n        $originalBaseTime = ($schedules | Where-Object { $_.Name.EndsWith(\"Weekly\") } | Sort-Object -Property StartTime | Select-Object -First 1).StartTime.AddHours(-1.25).DateTime\n        $now = (Get-Date).ToUniversalTime()\n        $diff = $now.AddHours(-1.25) - $originalBaseTime\n        $nextWeekDays = [Math]::Ceiling($diff.TotalDays / 7) * 7\n        $baseTime = $now.AddHours(-1.25).AddDays($nextWeekDays - $diff.TotalDays).ToString(\"u\")\n        Write-Host \"Existing schedules found. Keeping original base time: $baseTime.\" -ForegroundColor Green\n    }\n    else {\n        Write-Host \"Automation schedules base time automatically set to $baseTime.\" -ForegroundColor Green\n    }\n    #endregion\n\n    if (-not($upgrading))\n    {\n        #region Template-based deployment\n        $jobSchedules = Get-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -ErrorAction SilentlyContinue\n        if ($jobSchedules.Count -gt 0) {\n            Write-Host \"Unregistering previous runbook schedules associations from $automationAccountName...\" -ForegroundColor Green\n            foreach ($jobSchedule in $jobSchedules) {\n                if ($jobSchedule.ScheduleName.StartsWith(\"AzureOptimization\")) {\n                    Unregister-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                        -JobScheduleId $jobSchedule.JobScheduleId -Force\n                }\n            }    \n        }\n    \n        Write-Host \"Deploying Azure Optimization Engine resources...\" -ForegroundColor Green\n        $deploymentTries = 0\n        $maxDeploymentTries = 2\n        $deploymentSucceeded = $false\n        do {\n            $deploymentTries++\n            try {\n                $deployment = New-AzDeployment -TemplateFile \".\\azuredeploy.bicep\" -templateLocation $TemplateUri.Replace(\"azuredeploy.bicep\", \"\") -Location $targetLocation -rgName $resourceGroupName -Name $deploymentName `\n                    -projectLocation $targetlocation -logAnalyticsReuse $logAnalyticsReuse -baseTime $baseTime `\n                    -logAnalyticsWorkspaceName $laWorkspaceName -logAnalyticsWorkspaceRG $laWorkspaceResourceGroup `\n                    -storageAccountName $storageAccountName -automationAccountName $automationAccountName `\n                    -sqlServerName $sqlServerName -sqlDatabaseName $sqlDatabaseName -cloudEnvironment $AzureEnvironment `\n                    -sqlAdminLogin $sqlAdmin -sqlAdminPassword $sqlPass -resourceTags $ResourceTags -WarningAction SilentlyContinue\n                $deploymentSucceeded = $true\n            }\n            catch {\n                if ($deploymentTries -ge $maxDeploymentTries) {\n                    Write-Host \"Failed deployment. Stop trying.\" -ForegroundColor Yellow\n                    throw $_\n                }\n                Write-Host \"Failed deployment. Trying once more...\" -ForegroundColor Yellow\n            }            \n        } while (-not($deploymentSucceeded) -and $deploymentTries -lt $maxDeploymentTries)\n\n        $spnId = $deployment.Outputs['automationPrincipalId'].Value \n        #endregion\n    }\n    else\n    {\n        #region Partial upgrade deployment\n        $upgradeManifest = Get-Content -Path \"./upgrade-manifest.json\" | ConvertFrom-Json\n        Write-Host \"Creating missing storage account containers...\" -ForegroundColor Green\n        $upgradeContainers = $upgradeManifest.dataCollection.container\n        foreach ($container in $upgradeContainers)\n        {\n            if (-not($container -in $containers.Name))\n            {\n                New-AzStorageContainer -Name $container -Context $sa.Context -Permission Off | Out-Null\n                Write-Host \"$container container created.\"\n            }\n        }\n\n        Write-Host \"Importing runbooks...\" -ForegroundColor Green\n        $allRunbooks = $upgradeManifest.baseIngest.runbook + $upgradeManifest.dataCollection.runbook + $upgradeManifest.recommendations.runbook + $upgradeManifest.remediations.runbook\n        $runbookBaseUri = $TemplateUri.Replace(\"azuredeploy.bicep\", \"\")\n        $topTemplateJson = \"{ `\"`$schema`\": `\"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#`\", \" + `\n            \"`\"contentVersion`\": `\"1.0.0.0`\", `\"resources`\": [\"\n        $bottomTemplateJson = \"] }\"\n        $runbookDeploymentTemplateJson = $topTemplateJson\n        for ($i = 0; $i -lt $allRunbooks.Count; $i++)\n        {\n            try {\n                Invoke-WebRequest -Uri ($runbookBaseUri + $allRunbooks[$i].name) | Out-Null\n                $runbookName = [System.IO.Path]::GetFilenameWithoutExtension($allRunbooks[$i].name)\n                $runbookJson = \"{ `\"name`\": `\"$automationAccountName/$runbookName`\", `\"type`\": `\"Microsoft.Automation/automationAccounts/runbooks`\", \" + `\n                \"`\"apiVersion`\": `\"2018-06-30`\", `\"location`\": `\"$targetLocation`\", `\"tags`\": $($ResourceTags | ConvertTo-Json), `\"properties`\": { \" + `\n                \"`\"runbookType`\": `\"PowerShell`\", `\"logProgress`\": false, `\"logVerbose`\": false, \" + `\n                \"`\"publishContentLink`\": { `\"uri`\": `\"$runbookBaseUri$($allRunbooks[$i].name)`\", `\"version`\": `\"$($allRunbooks[$i].version)`\" } } }\"\n                $runbookDeploymentTemplateJson += $runbookJson\n                if ($i -lt $allRunbooks.Count - 1)\n                {\n                    $runbookDeploymentTemplateJson += \", \"\n                }\n                Write-Host \"$($allRunbooks[$i].name) imported.\"\n            }\n            catch {\n                Write-Host \"$($allRunbooks[$i].name) not imported (not found).\" -ForegroundColor Yellow\n            }\n        }\n        $runbookDeploymentTemplateJson += $bottomTemplateJson\n        $runbooksTemplatePath = \"./aoe-runbooks-deployment.json\"\n        $runbookDeploymentTemplateJson | Out-File -FilePath $runbooksTemplatePath -Force\n        Write-Host \"Executing runbooks deployment...\" -ForegroundColor Green\n        New-AzResourceGroupDeployment -TemplateFile $runbooksTemplatePath -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f \"runbooks\") | Out-Null\n        Remove-Item -Path $runbooksTemplatePath -Force\n        Write-Host \"Runbooks update deployed.\"\n\n        Write-Host \"Importing modules...\" -ForegroundColor Green\n        $allModules = $upgradeManifest.modules\n        $modulesDeploymentTemplateJson = $topTemplateJson\n        for ($i = 0; $i -lt $allModules.Count; $i++)\n        {\n            $moduleJson = \"{ `\"name`\": `\"$automationAccountName/$($allModules[$i].name)`\", `\"type`\": `\"Microsoft.Automation/automationAccounts/modules`\", \" + `\n                \"`\"apiVersion`\": `\"2018-06-30`\", `\"location`\": `\"$targetLocation`\", `\"tags`\": $($ResourceTags | ConvertTo-Json), `\"properties`\": { \" + `\n                \"`\"contentLink`\": { `\"uri`\": `\"$($allModules[$i].url)`\" } } \"\n            if ($allModules[$i].name -ne \"Az.Accounts\" -and $allModules[$i].name -ne \"Microsoft.Graph.Authentication\")\n            {\n                $moduleJson += \", `\"dependsOn`\": [ `\"Az.Accounts`\", `\"Microsoft.Graph.Authentication`\" ]\"\n            }\n            $moduleJson += \"}\"\n            $modulesDeploymentTemplateJson += $moduleJson\n            if ($i -lt $allModules.Count - 1)\n            {\n                $modulesDeploymentTemplateJson += \", \"\n            }\n            Write-Host \"$($allModules[$i].name) imported.\"\n        }\n        $modulesDeploymentTemplateJson += $bottomTemplateJson\n        $modulesTemplatePath = \"./aoe-modules-deployment.json\"\n        $modulesDeploymentTemplateJson | Out-File -FilePath $modulesTemplatePath -Force\n        Write-Host \"Executing modules deployment...\" -ForegroundColor Green\n        New-AzResourceGroupDeployment -TemplateFile $modulesTemplatePath -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f \"modules\") | Out-Null\n        Remove-Item -Path $modulesTemplatePath -Force\n        Write-Host \"Modules update deployed.\"\n\n        Write-Host \"Updating schedules...\" -ForegroundColor Green\n        $allSchedules = $upgradeManifest.schedules\n\n        $allScheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName\n        $exportHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Export\") })[0].HybridWorker\n        $ingestHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Ingest\") })[0].HybridWorker\n        $recommendHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Recommend\") })[0].HybridWorker\n        if ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Remediate\") })\n        {\n            $remediateHybridWorkerOption = ($allScheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Remediate\") })[0].HybridWorker\n        }\n        \n        $hybridWorkerOption = \"None\"\n        if ($exportHybridWorkerOption -or $ingestHybridWorkerOption -or $recommendHybridWorkerOption -or $remediateHybridWorkerOption) {\n            $hybridWorkerOption = \"Export: $exportHybridWorkerOption; Ingest: $ingestHybridWorkerOption; Recommend: $recommendHybridWorkerOption; Remediate: $remediateHybridWorkerOption\"\n        }      \n        Write-Host \"Current Hybrid Worker option: $hybridWorkerOption\" -ForegroundColor Green            \n\n        $dataIngestRunbookName = [System.IO.Path]::GetFileNameWithoutExtension(($upgradeManifest.baseIngest | Where-Object { $_.source -eq \"dataCollection\"}).runbook.name)\n        $dataExportsToMultiSchedule = $upgradeManifest.dataCollection | Where-Object { $_.exportSchedules.Count -gt 0 }\n        $recommendationsProcessingRunbooks = $upgradeManifest.baseIngest | Where-Object { $_.source -eq \"recommendations\" -or $_.source -eq \"maintenance\"}\n\n        foreach ($schedule in $allSchedules)\n        {\n            if (-not($schedules | Where-Object { $_.Name -eq $schedule.name }))\n            {\n                $scheduleStartTime = (Get-Date $baseTime).Add([System.Xml.XmlConvert]::ToTimeSpan($schedule.offset))\n                $scheduleNow = (Get-Date).ToUniversalTime()\n\n                if ($schedule.frequency -eq \"Hour\")\n                {\n                    if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)\n                    {\n                        $hoursDiff = ($scheduleNow - $scheduleStartTime).Hours + 1\n                        $scheduleStartTime = $scheduleStartTime.AddHours($hoursDiff)\n                    }\n\n                    New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `\n                        -StartTime $scheduleStartTime -HourInterval 1 | Out-Null\n                }\n                if ($schedule.frequency -eq \"Day\")\n                {\n                    if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)\n                    {\n                        $scheduleStartTime = $scheduleStartTime.AddDays(1)\n                    }\n\n                    New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `\n                        -StartTime $scheduleStartTime -DayInterval 1 | Out-Null\n                }\n                if ($schedule.frequency -eq \"Week\")\n                {\n                    if ($scheduleNow.AddMinutes(5) -gt $scheduleStartTime)\n                    {\n                        $scheduleStartTime = $scheduleStartTime.AddDays(7)\n                    }\n\n                    New-AzAutomationSchedule -Name $schedule.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `\n                        -StartTime $scheduleStartTime -WeekInterval 1 | Out-Null\n                }\n                Write-Host \"$($schedule.name) schedule created.\"\n            }\n\n            $scheduledRunbooks = Get-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                -ScheduleName $schedule.name\n\n            $dataExportsToSchedule = ($upgradeManifest.dataCollection + $upgradeManifest.recommendations) | Where-Object { $_.exportSchedule -eq $schedule.name }\n            foreach ($dataExport in $dataExportsToSchedule)\n            {\n                $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($dataExport.runbook.name)\n                $runbookType = $runbookName.Split(\"-\")[0]\n                switch ($runbookType)\n                {\n                    \"Export\" {\n                        $hybridWorkerName = $exportHybridWorkerOption\n                    }\n                    \"Recommend\" {\n                        $hybridWorkerName = $recommendHybridWorkerOption\n                    }\n                    \"Ingest\" {\n                        $hybridWorkerName = $ingestHybridWorkerOption\n                    }\n                    \"Remediate\" {\n                        $hybridWorkerName = $remediateHybridWorkerOption\n                    }\n                    Default {\n                        $hybridWorkerName = $null\n                    }\n                }\n\n                if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName}))\n                {\n                    if ($hybridWorkerName)\n                    {\n                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                            -RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName | Out-Null\n                    }\n                    else\n                    {\n                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                            -RunbookName $runbookName -ScheduleName $schedule.name | Out-Null                        \n                    }\n                    Write-Host \"Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook\"\n                }\n            }\n\n            foreach ($dataExport in $dataExportsToMultiSchedule)\n            {\n                $exportSchedule = $dataExport.exportSchedules | Where-Object { $_.schedule -eq $schedule.name }\n                if ($exportSchedule)\n                {\n                    $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($dataExport.runbook.name)\n                    $runbookType = $runbookName.Split(\"-\")[0]\n                    switch ($runbookType)\n                    {\n                        \"Export\" {\n                            $hybridWorkerName = $exportHybridWorkerOption\n                        }\n                        \"Recommend\" {\n                            $hybridWorkerName = $recommendHybridWorkerOption\n                        }\n                        \"Ingest\" {\n                            $hybridWorkerName = $ingestHybridWorkerOption\n                        }\n                        \"Remediate\" {\n                            $hybridWorkerName = $remediateHybridWorkerOption\n                        }\n                        Default {\n                            $hybridWorkerName = $null\n                        }\n                    }\n                    \n                    if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName -and $_.ScheduleName -eq $schedule.name}))\n                    {   \n                        $params = @{}\n                        $exportSchedule.parameters.PSObject.Properties | ForEach-Object {\n                            $params[$_.Name] = $_.Value\n                        }                                \n    \n                        if ($hybridWorkerName)\n                        {\n                            Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                                -RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName -Parameters $params | Out-Null\n                        }\n                        else\n                        {\n                            Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                                -RunbookName $runbookName -ScheduleName $schedule.name -Parameters $params | Out-Null                        \n                        }\n                        Write-Host \"Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook.\"\n                    }    \n                }\n            }\n\n            $dataIngestToSchedule = $upgradeManifest.dataCollection | Where-Object { $_.ingestSchedule -eq $schedule.name }\n            foreach ($dataIngest in $dataIngestToSchedule)\n            {\n                $hybridWorkerName = $ingestHybridWorkerOption\n    \n                if (-not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $dataIngestRunbookName}))\n                {\n                    $params = @{\"StorageSinkContainer\"=$dataIngest.container}\n\n                    if ($hybridWorkerName)\n                    {\n                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                            -RunbookName $dataIngestRunbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName -Parameters $params | Out-Null\n                    }\n                    else\n                    {\n                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                            -RunbookName $dataIngestRunbookName -ScheduleName $schedule.name -Parameters $params | Out-Null                        \n                    }\n                    Write-Host \"Added $($schedule.name) schedule to $hybridWorkerName $dataIngestRunbookName runbook.\"\n                }\n            }\n\n            foreach ($recommendationsProcessingRunbook in $recommendationsProcessingRunbooks)\n            {\n                $runbookName = [System.IO.Path]::GetFileNameWithoutExtension($recommendationsProcessingRunbook.runbook.name)\n                $hybridWorkerName = $ingestHybridWorkerOption\n    \n                if ($recommendationsProcessingRunbook.schedule -eq $schedule.name -and -not($scheduledRunbooks | Where-Object { $_.RunbookName -eq $runbookName}))\n                {\n                    if ($hybridWorkerName)\n                    {\n                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                            -RunbookName $runbookName -ScheduleName $schedule.name -RunOn $hybridWorkerName | Out-Null\n                    }\n                    else\n                    {\n                        Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                            -RunbookName $runbookName -ScheduleName $schedule.name | Out-Null                        \n                    }\n                    Write-Host \"Added $($schedule.name) schedule to $hybridWorkerName $runbookName runbook.\"\n                }\n            }\n        }\n\n        Write-Host \"Updating variables...\" -ForegroundColor Green\n        $allVariables = $upgradeManifest.dataCollection.requiredVariables + $upgradeManifest.recommendations.requiredVariables + $upgradeManifest.remediations.requiredVariables\n        foreach ($variable in $allVariables)\n        {\n            $existingVariables = Get-AzAutomationVariable -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName\n            if (-not($existingVariables | Where-Object { $_.Name -eq $variable.name }))\n            {\n                New-AzAutomationVariable -Name $variable.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `\n                    -Value $variable.defaultValue -Encrypted $false | Out-Null\n                Write-Host \"$($variable.name) variable created.\"\n            }\n        }\n\n        Write-Host \"Force-updating variables...\" -ForegroundColor Green\n        $forceUpdateVariables = $upgradeManifest.overwriteVariables\n        foreach ($variable in $forceUpdateVariables)\n        {\n            Set-AzAutomationVariable -Name $variable.name -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `\n                -Value $variable.value -Encrypted $false | Out-Null\n            Write-Host \"$($variable.name) variable updated.\"\n        }\n\n        Write-Host \"Removing deprecated runbooks...\" -ForegroundColor Green\n        $deprecatedRunbooks = $upgradeManifest.deprecatedRunbooks\n        foreach ($deprecatedRunbook in $deprecatedRunbooks)\n        {\n            Remove-AzAutomationRunbook -AutomationAccountName $automationAccountName -Name $deprecatedRunbook -ResourceGroupName $resourceGroupName -Force -ErrorAction SilentlyContinue\n        }\n        #endregion\n    }\n\n    #region Schedules reset\n    if ($upgradingSchedules) {\n        $schedules = Get-AzAutomationSchedule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName\n        $dailySchedules = $schedules | Where-Object { $_.Frequency -eq \"Day\" -or $_.Frequency -eq \"Hour\" }\n        Write-Host \"Fixing daily schedules after upgrade...\" -ForegroundColor Green\n        foreach ($schedule in $dailySchedules) {\n            $now = (Get-Date).ToUniversalTime()\n            $newStartTime = [System.DateTimeOffset]::Parse($now.ToString(\"yyyy-MM-ddT00:00:00Z\"))\n            $newStartTime = $newStartTime.AddHours($schedule.StartTime.Hour).AddMinutes($schedule.StartTime.Minute)\n            if ($newStartTime.AddMinutes(-5) -lt $now) {\n                $newStartTime = $newStartTime.AddDays(1)\n            }\n            $expiryTime = $schedule.ExpiryTime.ToString(\"yyyy-MM-ddTHH:mm:ssZ\")\n            $startTime = $newStartTime.ToString(\"yyyy-MM-ddTHH:mm:ssZ\")\n            $automationPath = \"/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/schedules/$($schedule.Name)?api-version=2015-10-31\"\n            $body = \"{\n                `\"name`\": `\"$($schedule.Name)`\",\n                `\"properties`\": {\n                  `\"description`\": `\"$($schedule.Description)`\",\n                  `\"startTime`\": `\"$startTime`\",\n                  `\"expiryTime`\": `\"$expiryTime`\",\n                  `\"interval`\": 1,\n                  `\"frequency`\": `\"$($schedule.Frequency.ToString())`\",\n                  `\"advancedSchedule`\": {}\n                }\n              }\"\n            Invoke-AzRestMethod -Path $automationPath -Method PUT -Payload $body | Out-Null\n        }\n    }\n    #endregion\n    \n    #region Deployment date Automation variable\n    Write-Host \"Checking Azure Automation variable referring to the initial Azure Optimization Engine deployment date...\" -ForegroundColor Green\n    $deploymentDateVariableName = \"AzureOptimization_DeploymentDate\"\n    $deploymentDateVariable = Get-AzAutomationVariable -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $deploymentDateVariableName -ErrorAction SilentlyContinue\n    \n    if ($null -eq $deploymentDateVariable) {\n        $deploymentDate = (Get-Date).ToUniversalTime().ToString(\"yyyy-MM-dd\")\n        Write-Host \"Setting initial deployment date ($deploymentDate)...\" -ForegroundColor Green\n        New-AzAutomationVariable -Name $deploymentDateVariableName -Description \"The date of the initial engine deployment\" `\n            -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Value $deploymentDate -Encrypted $false\n    }\n    #endregion\n\n    #region Open SQL Server firewall rule\n    if (-not($sqlServerName -like \"*.database.*\"))\n    {\n        $myPublicIp = (Invoke-WebRequest -uri \"https://ifconfig.me/ip\").Content.Trim()\n        if (-not($myPublicIp -like \"*.*.*.*\"))\n        {\n            $myPublicIp = (Invoke-WebRequest -uri \"https://ipv4.icanhazip.com\").Content.Trim()\n            if (-not($myPublicIp -like \"*.*.*.*\"))\n            {\n                $myPublicIp = (Invoke-WebRequest -uri \"https://ipinfo.io/ip\").Content.Trim()\n            }\n        }\n\n        Write-Host \"Opening SQL Server firewall temporarily to your public IP ($myPublicIp)...\" -ForegroundColor Green\n        $tempFirewallRuleName = \"InitialDeployment\"            \n        New-AzSqlServerFirewallRule -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -FirewallRuleName $tempFirewallRuleName -StartIpAddress $myPublicIp -EndIpAddress $myPublicIp -ErrorAction Continue | Out-Null\n    }\n    #endregion\n    \n    #region SQL Database model deployment\n    Write-Host \"Deploying SQL Database model...\" -ForegroundColor Green\n    \n    $sqlPassPlain = (New-Object PSCredential \"user\", $sqlPass).GetNetworkCredential().Password     \n    if (-not($sqlServerName -like \"*.database.*\"))\n    {\n        $sqlServerEndpoint = \"$sqlServerName$($cloudDetails.SqlDatabaseDnsSuffix)\"\n    }\n    else \n    {\n        $sqlServerEndpoint = $sqlServerName\n    }\n    $databaseName = $sqlDatabaseName\n    $SqlTimeout = 60\n    $tries = 0\n    $connectionSuccess = $false\n    do {\n        $tries++\n        try {\n    \n    \n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $createTableQuery = Get-Content -Path \"./model/loganalyticsingestcontrol-table.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $createTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n    \n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $initTableQuery = Get-Content -Path \"./model/loganalyticsingestcontrol-initialize.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $initTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n    \n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $upgradeTableQuery = Get-Content -Path \"./model/loganalyticsingestcontrol-upgrade.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $upgradeTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n    \n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $createTableQuery = Get-Content -Path \"./model/sqlserveringestcontrol-table.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $createTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n\n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $initTableQuery = Get-Content -Path \"./model/sqlserveringestcontrol-initialize.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $initTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n\n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $createTableQuery = Get-Content -Path \"./model/recommendations-table.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $createTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n\n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $createTableQuery = Get-Content -Path \"./model/recommendations-sp.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $createTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n\n            $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlServerEndpoint,1433;Database=$databaseName;User ID=$sqlAdmin;Password=$sqlPassPlain;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n            $Conn.Open() \n    \n            $createTableQuery = Get-Content -Path \"./model/filters-table.sql\"\n            $Cmd = new-object system.Data.SqlClient.SqlCommand\n            $Cmd.Connection = $Conn\n            $Cmd.CommandTimeout = $SqlTimeout\n            $Cmd.CommandText = $createTableQuery\n            $Cmd.ExecuteReader()\n            $Conn.Close()\n\n            $connectionSuccess = $true\n        }\n        catch {\n            Write-Host \"Failed to contact SQL at try $tries.\" -ForegroundColor Yellow\n            Write-Host $Error[0] -ForegroundColor Yellow\n            Start-Sleep -Seconds ($tries * 20)\n        }    \n    } while (-not($connectionSuccess) -and $tries -lt 3)\n    \n    if (-not($connectionSuccess)) {\n        if (-not($sqlServerName -like \"*.database.*\"))\n        {\n            Write-Host \"Deleting temporary SQL Server firewall rule...\" -ForegroundColor Green\n            Remove-AzSqlServerFirewallRule -FirewallRuleName $tempFirewallRuleName -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -ErrorAction Continue | Out-Null\n        }    \n        throw \"Could not establish connection to SQL.\"\n    }\n    #endregion\n    \n    #region Close SQL Server firewall rule\n    if (-not($sqlServerName -like \"*.database.*\"))\n    {\n        Write-Host \"Deleting temporary SQL Server firewall rule...\" -ForegroundColor Green\n        Remove-AzSqlServerFirewallRule -FirewallRuleName $tempFirewallRuleName -ResourceGroupName $resourceGroupName -ServerName $sqlServerName -ErrorAction Continue  | Out-Null\n    }    \n    #endregion\n\n    #region Workbooks deployment\n    if (-not($deploymentOptions[\"DeployWorkbooks\"]))\n    {\n        $deployWorkbooks = Read-Host \"Do you want to deploy the workbooks with additional insights (recommended)? (Y/N)\"\n    }\n    else\n    {\n        $deployWorkbooks = $deploymentOptions[\"DeployWorkbooks\"]\n    }\n    if (\"Y\", \"y\" -contains $deployWorkbooks) {\n        $deploymentOptions[\"DeployWorkbooks\"] = \"Y\"\n        $deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force\n        Write-Host \"Publishing workbooks...\" -ForegroundColor Green\n        $workbooks = Get-ChildItem -Path \"./views/workbooks/\" | Where-Object { $_.Name.EndsWith(\".bicep\") }\n        $la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $laWorkspaceResourceGroup -Name $laWorkspaceName\n        foreach ($workbook in $workbooks)\n        {\n            $workbookFileName = [System.IO.Path]::GetFileNameWithoutExtension($workbook.Name)\n            Write-Host \"Deploying $workbookFileName workbook...\"\n            try {\n                New-AzResourceGroupDeployment -TemplateFile $workbook.FullName -ResourceGroupName $resourceGroupName -Name ($deploymentNameTemplate -f $workbookFileName) `\n                    -workbookSourceId $la.ResourceId -resourceTags $ResourceTags -WarningAction SilentlyContinue | Out-Null        \n            }\n            catch {\n                Write-Host \"Failed to deploy the workbook. If you are upgrading AOE, please remove first the $workbookFileName workbook from the $laWorkspaceName Log Analytics workspace and then re-deploy.\" -ForegroundColor Yellow            \n            }\n        }\n    }\n    #endregion\n\n    if (!$silentDeploy)\n    {\n        #region Grant Microsoft Entra ID role to AOE principal\n        if ($null -eq $spnId)\n        {\n            $auto = Get-AzAutomationAccount -Name $automationAccountName -ResourceGroupName $resourceGroupName\n            $spnId = $auto.Identity.PrincipalId\n            if ($null -eq $spnId)\n            {\n                $runAsConnection = Get-AzAutomationConnection -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name AzureRunAsConnection -ErrorAction SilentlyContinue\n                $runAsAppId = $runAsConnection.FieldDefinitionValues.ApplicationId\n                if ($runAsAppId)\n                {\n                    $runAsServicePrincipal = Get-AzADServicePrincipal -ApplicationId $runAsAppId\n                    $spnId = $runAsServicePrincipal.Id\n                }\n            }\n        }\n\n        try\n        {\n            Import-Module Microsoft.Graph.Authentication\n            Import-Module Microsoft.Graph.Identity.DirectoryManagement\n\n            Write-Host \"Granting Microsoft Entra ID Global Reader role to the Automation Account (requires administrative permissions in Microsoft Entra and MS Graph PowerShell SDK >= 2.4.0)...\" -ForegroundColor Green\n\n            #workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888\n            $localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)\n            if (-not(get-item \"$localPath\\.graph\\\" -ErrorAction SilentlyContinue))\n            {\n                New-Item -Type Directory \"$localPath\\.graph\"\n            }\n            \n            switch ($cloudEnvironment) {\n                \"AzureUSGovernment\" {  \n                    $graphEnvironment = \"USGov\"\n                    break\n                }\n                \"AzureChinaCloud\" {  \n                    $graphEnvironment = \"China\"\n                    break\n                }\n                \"AzureGermanCloud\" {  \n                    $graphEnvironment = \"Germany\"\n                    break\n                }\n                Default {\n                    $graphEnvironment = \"Global\"\n                }\n            }\n            \n            Connect-MgGraph -Scopes \"RoleManagement.ReadWrite.Directory\",\"Directory.Read.All\" -UseDeviceAuthentication -Environment $graphEnvironment -NoWelcome\n            \n            $globalReaderRole = Get-MgDirectoryRole -ExpandProperty Members -Property Id,Members,DisplayName,RoleTemplateId `\n                | Where-Object { $_.RoleTemplateId -eq \"f2ef992c-3afb-46b9-b7cf-a126ee74c451\" }\n            $globalReaders = $globalReaderRole.Members.Id\n            if (-not($globalReaders -contains $spnId))\n            {\n                New-MgDirectoryRoleMemberByRef -DirectoryRoleId $globalReaderRole.Id -BodyParameter @{\"@odata.id\" = \"https://graph.microsoft.com/v1.0/directoryObjects/$spnId\"}\n                Start-Sleep -Seconds 5\n                $globalReaderRole = Get-MgDirectoryRole -ExpandProperty Members -Property Id,Members,DisplayName,RoleTemplateId `\n                    | Where-Object { $_.RoleTemplateId -eq \"f2ef992c-3afb-46b9-b7cf-a126ee74c451\" }\n                $globalReaders = $globalReaderRole.Members.Id\n                if ($globalReaders -contains $spnId)\n                {\n                    Write-Host \"Role granted.\" -ForegroundColor Green\n                }\n                else\n                {\n                    throw \"Error when trying to grant Global Reader role\"\n                }\n            }\n            else\n            {\n                Write-Host \"Role was already granted before.\" -ForegroundColor Green            \n            }        \n        }\n        catch\n        {\n            Write-Host $Error[0] -ForegroundColor Yellow\n            Write-Host \"Could not grant role. If you want Microsoft Entra-based recommendations, please grant the Global Reader role manually to the $automationAccountName managed identity or, for previous versions of AOE, to the Run As Account principal.\" -ForegroundColor Red\n        }\n        #endregion\n    }\n    else\n    {\n        Write-Host \"Could not grant role. If you want Microsoft Entra-based recommendations, please grant the Global Reader role manually to the $automationAccountName managed identity or, for previous versions of AOE, to the Run As Account principal.\" -ForegroundColor Red\n    }\n\n    Write-Host \"Azure Optimization Engine deployment completed! We're almost there...\" -ForegroundColor Green\n\n    #region Benefits Usage dependencies\n    if (-not($deploymentOptions[\"DeployBenefitsUsageDependencies\"]))\n    {\n        $benefitsUsageDependenciesOption = Read-Host \"Do you also want to deploy the dependencies for the Azure Benefits usage workbooks (EA/MCA customers only + agreement administrator role required)? (Y/N)\"\n    } \n    else \n    {\n        $benefitsUsageDependenciesOption = $deploymentOptions[\"DeployBenefitsUsageDependencies\"]\n    }\n    if (\"Y\", \"y\" -contains $benefitsUsageDependenciesOption) \n    {\n        $deploymentOptions[\"DeployBenefitsUsageDependencies\"] = $benefitsUsageDependenciesOption        \n        $automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName\n        $principalId = $automationAccount.Identity.PrincipalId\n        $tenantId = $automationAccount.Identity.TenantId\n\n        $mcaBillingAccountIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n        $mcaBillingProfileIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n        \n        if (-not($deploymentOptions[\"CustomerType\"]))\n        {   \n            $customerType = Read-Host \"Are you an Enterprise Agreement (EA) or Microsoft Customer Agreement (MCA) customer? Please, type EA or MCA\"\n            $deploymentOptions[\"CustomerType\"] = $customerType        \n        }\n        else \n        {\n            $customerType = $deploymentOptions[\"CustomerType\"]\n        }\n        \n        switch ($customerType) {\n            \"EA\" {  \n                if (-not($deploymentOptions[\"BillingAccountId\"]))\n                {\n                    $billingAccountId = Read-Host \"Please, enter your Enterprise Agreement Billing Account ID (e.g. 12345678)\"\n                    $deploymentOptions[\"BillingAccountId\"] = $billingAccountId\n                }\n                else \n                {\n                    $billingAccountId = $deploymentOptions[\"BillingAccountId\"]\n                }\n                try\n                {\n                    [int32]::Parse($billingAccountId) | Out-Null\n                }\n                catch\n                {\n                    throw \"The Enterprise Agreement Billing Account ID must be a number (e.g. 12345678).\"\n                }\n                Write-Host \"Granting the Enterprise Enrollment Reader role to the AOE Managed Identity...\" -ForegroundColor Green\n                $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments?api-version=2019-10-01-preview\"\n                $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri\n                if (-not($roleAssignmentResponse.StatusCode -eq 200))\n                {\n                    throw \"The Enterprise Enrollment Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n                }\n                $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value\n                if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq \"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e\" }))\n                {\n                    $billingRoleAssignmentName = ([System.Guid]::NewGuid()).Guid\n                    $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments/$($billingRoleAssignmentName)?api-version=2019-10-01-preview\"\n                    $body = \"{`\"properties`\": {`\"principalId`\":`\"$principalId`\",`\"principalTenantId`\":`\"$tenantId`\",`\"roleDefinitionId`\":`\"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e`\"}}\"\n                    $roleAssignmentResponse = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body\n                    if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))\n                    {\n                        throw \"The Enterprise Enrollment Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n                    }\n                }\n                else\n                {\n                    Write-Host \"Role was already granted before.\" -ForegroundColor Green\n                }\n                break\n            }\n            \"MCA\" {\n                if (-not($deploymentOptions[\"BillingAccountId\"]))\n                {\n                    $billingAccountId = Read-Host \"Please, enter your Microsoft Customer Agreement Billing Account ID (e.g. <guid>:<guid>_YYYY-MM-DD)\"\n                    $deploymentOptions[\"BillingAccountId\"] = $billingAccountId\n                }\n                else \n                {\n                    $billingAccountId = $deploymentOptions[\"BillingAccountId\"]\n                }\n                if (-not($billingAccountId -match $mcaBillingAccountIdRegex))\n                {\n                    throw \"The Microsoft Customer Agreement Billing Account ID must be in the format <guid>:<guid>_YYYY-MM-DD.\"\n                }\n                if (-not($deploymentOptions[\"BillingProfileId\"]))\n                {\n                    $billingProfileId = Read-Host \"Please, enter your Billing Profile ID (e.g. ABCD-DEF-GHI-JKL)\"\n                    $deploymentOptions[\"BillingProfileId\"] = $billingProfileId\n                }\n                else \n                {\n                    $billingProfileId = $deploymentOptions[\"BillingProfileId\"]\n                }\n                if (-not($billingProfileId -match $mcaBillingProfileIdRegex))\n                {\n                    throw \"The Microsoft Customer Agreement Billing Profile ID must be in the format ABCD-DEF-GHI-JKL.\"\n                }\n                Write-Host \"Granting the Billing Profile Reader role to the AOE Managed Identity...\" -ForegroundColor Green\n                $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleAssignments?api-version=2019-10-01-preview\"\n                $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri\n                if (-not($roleAssignmentResponse.StatusCode -eq 200))\n                {\n                    throw \"The Billing Profile Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n                }\n                $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value\n                if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq \"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002\" }))\n                {\n                    $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/createBillingRoleAssignment?api-version=2020-12-15-privatepreview\"\n                    $body = \"{`\"principalId`\":`\"$principalId`\",`\"principalTenantId`\":`\"$tenantId`\",`\"roleDefinitionId`\":`\"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002`\"}\"\n                    $roleAssignmentResponse = Invoke-AzRestMethod -Method POST -Uri $uri -Payload $body\n                    if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))\n                    {\n                        throw \"The Billing Profile Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n                    }    \n                }\n                else\n                {\n                    Write-Host \"Role was already granted before.\" -ForegroundColor Green\n                }\n                break\n            }\n            Default {\n                throw \"Only EA and MCA customers are supported at this time.\"\n            }\n        }\n        \n        Write-Output \"Setting up the Billing Account ID variable...\"\n        $billingAccountIdVarName = \"AzureOptimization_BillingAccountID\"\n        $billingAccountIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -ErrorAction SilentlyContinue\n        if (-not($billingAccountIdVar))\n        {\n            New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null\n        }\n        else\n        {\n            Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null\n        }\n        \n        if ($billingProfileId)\n        {\n            Write-Output \"Setting up the Billing Profile ID variable...\"\n            $billingProfileIdVarName = \"AzureOptimization_BillingProfileID\"\n            $billingProfileIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -ErrorAction SilentlyContinue\n            if (-not($billingProfileIdVar))\n            {\n                New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null\n            }\n            else\n            {\n                Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null\n            }    \n        }    \n\n        if (-not $deploymentOptions[\"CurrencyCode\"])\n        {\n            $currencyCode = Read-Host \"Please, enter your consumption currency code (e.g. EUR, USD, etc.)\"\n            $deploymentOptions[\"CurrencyCode\"] = $currencyCode\n        }\n        else \n        {\n            $currencyCode = $deploymentOptions[\"CurrencyCode\"]\n        }\n\n        $deploymentOptions | ConvertTo-Json | Out-File -FilePath $lastDeploymentStatePath -Force\n\n        Write-Output \"Setting up the consumption currency code variable...\"\n        $currencyCodeVarName = \"AzureOptimization_RetailPricesCurrencyCode\"\n        $currencyCodeVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -ErrorAction SilentlyContinue\n        if (-not($currencyCodeVar))\n        {\n            New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null\n        }\n        else\n        {\n            Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null\n        }\n    }    \n    #endregion\n\n    Write-Host \"Deployment fully completed!\" -ForegroundColor Green\n}\nelse {\n    Write-Host \"Deployment cancelled.\" -ForegroundColor Red\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Hélder Pinto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Welcome to the Azure Optimization Engine - now a FinOps Toolkit tool! 🔍\n\n👋 Thank you for your interest in the Azure Optimization Engine! We've migrated the Azure Optimization Engine repository to a new home - the Microsoft [FinOps Toolkit](https://aka.ms/AzureOptimizationEngine). For historical reasons, the older code will remain here, but the most recent version and new releases will be made in the FinOps Toolkit repository going forward. Here's what you need to know:\n\n1. **We are collecting feedback in preparation for AOE v2**: Answer this [anonymous feedback form](https://forms.office.com/r/fLeJS8Rd2E) and contribute to the [discussion about the evolution of AOE](https://github.com/microsoft/finops-toolkit/discussions/753).\n\n1. **Start Using the FinOps Toolkit**: If you were using the Azure Optimization Engine, it's time to switch! The [FinOps Toolkit](https://aka.ms/AzureOptimizationEngine) provides advanced solutions, automation scripts, and learning resources to accelerate your FinOps journey.\n\n2. **Open Issues Here**: Have questions, feedback, or need assistance? Open an issue in the [FinOps Toolkit repository](https://github.com/microsoft/finops-toolkit/issues), and our community and maintainers will be happy to help. 🙌\n\n3. **Explore Our Tools**: Besides the Azure Optimization Engine, check out all the other available tools, including FinOps hubs, Power BI reports, cost optimization workbooks, and more. [Explore the FinOps Toolkit](https://aka.ms/finops/toolkit)\n\n4. **Stay Updated**: Follow our [FinOps blog](https://techcommunity.microsoft.com/t5/finops-blog/bg-p/FinOpsBlog) for the latest news and announcements.\n\nLet's make cloud cost management easier together! 🌟\n"
  },
  {
    "path": "Reset-AutomationSchedules.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $true)] \n    [String] $AutomationAccountName,\n\n    [Parameter(Mandatory = $true)] \n    [String] $ResourceGroupName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$ctx = Get-AzContext\nif (-not($ctx)) {\n    Connect-AzAccount -Environment $AzureEnvironment\n    $ctx = Get-AzContext\n}\nelse {\n    if ($ctx.Environment.Name -ne $AzureEnvironment) {\n        Disconnect-AzAccount -ContextName $ctx.Name\n        Connect-AzAccount -Environment $AzureEnvironment\n        $ctx = Get-AzContext\n    }\n}\n\ntry {\n    $scheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName\n}\ncatch {\n    throw \"$AutomationAccountName Automation Account not found in Resource Group $ResourceGroupName in Subscription $($ctx.Subscription.Name). If we are not in the right subscription, use Set-AzContext to switch to the correct one.\"    \n}\n\nif (-not($scheduledRunbooks)) {\n    throw \"The $AutomationAccountName Automation Account does not contain any scheduled runbook. It might not be associated to the Azure Optimization Engine.\"\n}\n\n$subscriptionId = (Get-AzContext).Subscription.Id\n\n$schedules = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName\n$weeklySchedules = $schedules | Where-Object { $_.Name.StartsWith(\"AzureOptimization\") -and $_.Name.EndsWith(\"Weekly\") }\nif ($weeklySchedules.Count -gt 0) {\n    $originalBaseTime = ($weeklySchedules | Sort-Object -Property StartTime | Select-Object -First 1).StartTime.AddHours(-1.25).DateTime\n    $now = (Get-Date).ToUniversalTime()\n    $diff = $now.AddHours(-1.25) - $originalBaseTime\n    $nextWeekDays = [Math]::Ceiling($diff.TotalDays / 7) * 7\n    $baseDateTime = $now.AddHours(-1.25).AddDays($nextWeekDays - $diff.TotalDays)\n    $baseTimeStr = $baseDateTime.ToString(\"u\")\n    Write-Host \"Existing schedules found. Weekly base time is $($baseDateTime.DayOfWeek) at $($baseDateTime.ToString('T')) (UTC).\" -ForegroundColor Green\n}\nelse {\n    throw \"The $AutomationAccountName Automation Account does not contain Azure Optimization Engine schedules.\"\n}\n\n$newBaseTimeStr = Read-Host \"Please, enter a new base time for the *weekly* schedules in UTC (YYYY-MM-dd HH:mm:ss). If you want to keep the current one, just press ENTER\"\nif (-not($newBaseTimeStr)) {\n    $newBaseTimeStr = $baseTimeStr\n}\nelse {\n    try {\n        $newBaseTimeStr += \"Z\"\n        $newBaseTime = [DateTime]::Parse($newBaseTimeStr)\n    }\n    catch {\n        throw \"$newBaseTimeStr is an invalid base time. Use the following format: YYYY-MM-dd HH:mm:ss. For example: 1977-09-08 06:14:15\"\n    }\n    if ($newBaseTime -lt (Get-Date).ToUniversalTime().AddHours(-1)) {\n        throw \"$newBaseTimeStr is an invalid base time. It can't be sooner than $((Get-Date).ToUniversalTime().AddHours(-1).ToString('u'))\"\n    }\n}\n\n$baseTimeUtc = [DateTime]::Parse($newBaseTimeStr).ToUniversalTime()\n\nif ($newBaseTimeStr -ne $baseTimeStr) {\n    Write-Host \"Updating current base schedule to every $($baseTimeUtc.DayOfWeek) at $($baseTimeUtc.TimeOfDay.ToString()) UTC...\" -ForegroundColor Green\n    $continueInput = Read-Host \"Continue (Y/N)?\"\n\n    if (\"Y\", \"y\" -contains $continueInput) {\n        $upgradeManifest = Get-Content -Path \"./upgrade-manifest.json\" | ConvertFrom-Json\n        $manifestSchedules = $upgradeManifest.schedules\n\n        foreach ($schedule in $schedules) {\n            $manifestSchedule = $manifestSchedules | Where-Object { $_.name -eq $schedule.Name }\n            if ($manifestSchedule) {\n                if ($schedule.Frequency -eq \"Week\") {\n                    $newStartTime = $baseTimeUtc.Add([System.Xml.XmlConvert]::ToTimeSpan($manifestSchedule.offset))\n                }\n                else {\n                    $now = (Get-Date).ToUniversalTime()\n                    $newStartTime = [System.DateTimeOffset]::Parse($now.ToString(\"yyyy-MM-ddT00:00:00Z\"))\n                    $newStartTime = $newStartTime.AddHours($baseTimeUtc.Hour).AddMinutes($baseTimeUtc.Minute).AddSeconds($baseTimeUtc.Second)\n                    if ($newStartTime -lt $now.AddMinutes(15))\n                    {\n                        $newStartTime = $newStartTime.AddDays(1)\n                    }\n                    $newStartTime = $newStartTime.Add([System.Xml.XmlConvert]::ToTimeSpan($manifestSchedule.offset))                \n                }\n                $expiryTime = $schedule.ExpiryTime.ToString(\"yyyy-MM-ddTHH:mm:ssZ\")\n                $startTime = $newStartTime.ToString(\"yyyy-MM-ddTHH:mm:ssZ\")\n                $automationPath = \"/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/schedules/$($schedule.Name)?api-version=2015-10-31\"\n                $body = \"{\n                `\"name`\": `\"$($schedule.Name)`\",\n                `\"properties`\": {\n                  `\"description`\": `\"$($schedule.Description)`\",\n                  `\"startTime`\": `\"$startTime`\",\n                  `\"expiryTime`\": `\"$expiryTime`\",\n                  `\"interval`\": $($schedule.Interval),\n                  `\"frequency`\": `\"$($schedule.Frequency)`\",\n                  `\"advancedSchedule`\": {}\n                }\n              }\"\n                Invoke-AzRestMethod -Path $automationPath -Method PUT -Payload $body | Out-Null    \n                Write-Host \"Re-scheduled $($schedule.Name).\" -ForegroundColor Blue\n            }\n            else {\n                Write-Host \"$($schedule.Name) not found in schedules manifest.\" -ForegroundColor Yellow\n            }\n        }\n    }\n    else\n    {\n        throw \"Interrupting schedules reset due to user input.\"   \n    }\n}\nelse {\n    Write-Host \"Kept current base schedule (every $($baseTimeUtc.DayOfWeek) at $($baseTimeUtc.TimeOfDay.ToString()) UTC).\" -ForegroundColor Green\n}\n\n$exportHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Export\") })[0].HybridWorker\n$ingestHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Ingest\") })[0].HybridWorker\n$recommendHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Recommend\") })[0].HybridWorker\nif ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Remediate\") })\n{\n    $remediateHybridWorkerOption = ($scheduledRunbooks | Where-Object { $_.RunbookName.StartsWith(\"Remediate\") })[0].HybridWorker\n}\n\n$hybridWorkerOption = \"None\"\nif ($exportHybridWorkerOption -or $ingestHybridWorkerOption -or $recommendHybridWorkerOption -or $remediateHybridWorkerOption) {\n    $hybridWorkerOption = \"Export: $exportHybridWorkerOption; Ingest: $ingestHybridWorkerOption; Recommend: $recommendHybridWorkerOption; Remediate: $remediateHybridWorkerOption\"\n}\n\nWrite-Host \"Current Hybrid Worker option: $hybridWorkerOption\" -ForegroundColor Green\n\n$newHybridWorker = Read-Host \"If you want all schedules to use the same Hybrid Worker, please enter the Hybrid Worker Group name (if you want to keep the current option, just press ENTER)\"\n\nif ($newHybridWorker)\n{\n    $hybridWorker = Get-AzAutomationHybridWorkerGroup -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $newHybridWorker -ErrorAction SilentlyContinue\n    if (-not($hybridWorker))\n    {\n        throw \"Hybrid Worker $newHybridWorker was not found in Automation Account $automationAccountName.\"   \n    }\n\n    Write-Host \"Updating Hybrid Worker Group in every runbook schedule to $newHybridWorker...\"    \n    $continueInput = Read-Host \"Continue (Y/N)?\"\n\n    if (\"Y\", \"y\" -contains $continueInput)\n    {\n        Write-Host \"Re-registering previous runbook schedules associations from $automationAccountName...\" -ForegroundColor Green\n        foreach ($jobSchedule in $scheduledRunbooks) {\n            if ($jobSchedule.ScheduleName.StartsWith(\"AzureOptimization\")) {\n                $automationPath = \"/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Automation/automationAccounts/$automationAccountName/jobSchedules/$($jobSchedule.JobScheduleId)?api-version=2015-10-31\"\n                $jobSchedule = (Invoke-AzRestMethod -Path $automationPath -Method GET).Content | ConvertFrom-Json\n                Unregister-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                    -JobScheduleId $jobSchedule.id.Split('/')[10] -Force\n                $params = @{}\n                $jobSchedule.properties.parameters.PSObject.Properties | ForEach-Object {\n                    $params[$_.Name] = $_.Value\n                }                                                \n                Register-AzAutomationScheduledRunbook -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName `\n                    -RunbookName $jobSchedule.properties.runbook.name -ScheduleName $jobSchedule.properties.schedule.name -RunOn $newHybridWorker -Parameters $params | Out-Null\n                Write-Host \"Re-registered $($jobSchedule.properties.runbook.name) for schedule $($jobSchedule.properties.schedule.name).\" -ForegroundColor Blue\n            }\n        }        \n    }\n    else\n    {\n        throw \"Interrupting schedules reset due to user input.\"   \n    }\n}\nelse\n{\n    Write-Host \"Kept current Hybrid Worker option: $hybridWorkerOption\" -ForegroundColor Green\n}\n\nWrite-Host \"DONE\" -ForegroundColor Green"
  },
  {
    "path": "Setup-BenefitsUsageDependencies.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $true)] \n    [String] $AutomationAccountName,\n\n    [Parameter(Mandatory = $true)] \n    [String] $ResourceGroupName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$ctx = Get-AzContext\nif (-not($ctx)) {\n    Connect-AzAccount -Environment $AzureEnvironment\n    $ctx = Get-AzContext\n}\nelse {\n    if ($ctx.Environment.Name -ne $AzureEnvironment) {\n        Disconnect-AzAccount -ContextName $ctx.Name\n        Connect-AzAccount -Environment $AzureEnvironment\n        $ctx = Get-AzContext\n    }\n}\n\ntry {\n    $scheduledRunbooks = Get-AzAutomationScheduledRunbook -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName\n}\ncatch {\n    throw \"$AutomationAccountName Automation Account not found in Resource Group $ResourceGroupName in Subscription $($ctx.Subscription.Name). If we are not in the right subscription, use Set-AzContext to switch to the correct one.\"    \n}\n\nif (-not($scheduledRunbooks)) {\n    throw \"The $AutomationAccountName Automation Account does not contain any scheduled runbook. It might not be associated to the Azure Optimization Engine.\"\n}\n\n$automationAccount = Get-AzAutomationAccount -ResourceGroupName $ResourceGroupName -Name $AutomationAccountName\n$principalId = $automationAccount.Identity.PrincipalId\n$tenantId = $automationAccount.Identity.TenantId\n\nif (-not($principalId))\n{\n    throw \"The $AutomationAccountName Automation Account does not have a managed identity and probably is not associated to the latest version of Azure Optimization Engine. Please, upgrade it before setting up benefits usage dependencies (more details: https://github.com/helderpinto/AzureOptimizationEngine#upgrade).\"\n}\n\n$pricesheetSchedule = Get-AzAutomationSchedule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name \"AzureOptimization_ExportPricesWeekly\" -ErrorAction SilentlyContinue\nif (-not($pricesheetSchedule)) {\n    throw \"The Azure Optimization Engine is in an older version. Please, upgrade it before setting up benefits usage dependencies (more details: https://github.com/helderpinto/AzureOptimizationEngine#upgrade).\"\n}\n\n$mcaBillingAccountIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n$mcaBillingProfileIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n\n$customerType = Read-Host \"Are you an Enterprise Agreement (EA), Microsoft Customer Agreement (MCA), or other type (Other) of customer? Please, type EA, MCA, or Other\"\n\nswitch ($customerType) {\n    \"EA\" {  \n        $billingAccountId = Read-Host \"Please, enter your Enterprise Agreement Billing Account ID (e.g. 12345678)\"\n        try\n        {\n            [int32]::Parse($billingAccountId) | Out-Null\n        }\n        catch\n        {\n            throw \"The Enterprise Agreement Billing Account ID must be a number (e.g. 12345678).\"\n        }\n        Write-Host \"Granting the Enterprise Enrollment Reader role to the AOE Managed Identity...\" -ForegroundColor Green\n        $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments?api-version=2019-10-01-preview\"\n        $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri\n        if (-not($roleAssignmentResponse.StatusCode -eq 200))\n        {\n            throw \"The Enterprise Enrollment Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n        }\n        $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value\n        if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq \"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e\" }))\n        {\n            $billingRoleAssignmentName = ([System.Guid]::NewGuid()).Guid\n            $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleAssignments/$($billingRoleAssignmentName)?api-version=2019-10-01-preview\"\n            $body = \"{`\"properties`\": {`\"principalId`\":`\"$principalId`\",`\"principalTenantId`\":`\"$tenantId`\",`\"roleDefinitionId`\":`\"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingRoleDefinitions/24f8edb6-1668-4659-b5e2-40bb5f3a7d7e`\"}}\"\n            $roleAssignmentResponse = Invoke-AzRestMethod -Method PUT -Uri $uri -Payload $body\n            if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))\n            {\n                throw \"The Enterprise Enrollment Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n            }\n        }\n        else\n        {\n            Write-Host \"Role was already granted before.\" -ForegroundColor Green\n        }\n        break\n    }\n    \"MCA\" {\n        $billingAccountId = Read-Host \"Please, enter your Microsoft Customer Agreement Billing Account ID (e.g. <guid>:<guid>_YYYY-MM-DD)\"\n        if (-not($billingAccountId -match $mcaBillingAccountIdRegex))\n        {\n            throw \"The Microsoft Customer Agreement Billing Account ID must be in the format <guid>:<guid>_YYYY-MM-DD.\"\n        }\n        $billingProfileId = Read-Host \"Please, enter your Billing Profile ID (e.g. ABCD-DEF-GHI-JKL)\"\n        if (-not($billingProfileId -match $mcaBillingProfileIdRegex))\n        {\n            throw \"The Microsoft Customer Agreement Billing Profile ID must be in the format ABCD-DEF-GHI-JKL.\"\n        }\n        Write-Host \"Granting the Billing Profile Reader role to the AOE Managed Identity...\" -ForegroundColor Green\n        $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleAssignments?api-version=2019-10-01-preview\"\n        $roleAssignmentResponse = Invoke-AzRestMethod -Method GET -Uri $uri\n        if (-not($roleAssignmentResponse.StatusCode -eq 200))\n        {\n            throw \"The Billing Profile Reader role could not be verified. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n        }\n        $roleAssignments = ($roleAssignmentResponse.Content | ConvertFrom-Json).value\n        if (-not($roleAssignments | Where-Object { $_.properties.principalId -eq $principalId -and $_.properties.roleDefinitionId -eq \"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002\" }))\n        {\n            $uri = \"https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/createBillingRoleAssignment?api-version=2020-12-15-privatepreview\"\n            $body = \"{`\"principalId`\":`\"$principalId`\",`\"principalTenantId`\":`\"$tenantId`\",`\"roleDefinitionId`\":`\"/providers/Microsoft.Billing/billingAccounts/$billingAccountId/billingProfiles/$billingProfileId/billingRoleDefinitions/40000000-aaaa-bbbb-cccc-100000000002`\"}\"\n            $roleAssignmentResponse = Invoke-AzRestMethod -Method POST -Uri $uri -Payload $body\n            if (-not($roleAssignmentResponse.StatusCode -in (200,201,202)))\n            {\n                throw \"The Billing Profile Reader role could not be granted. Status Code: $($roleAssignmentResponse.StatusCode); Response: $($roleAssignmentResponse.Content)\"\n            }    \n        }\n        else\n        {\n            Write-Host \"Role was already granted before.\" -ForegroundColor Green\n        }\n        break\n    }\n    Default {\n        throw \"Only EA and MCA customers are supported at this time.\"\n    }\n}\n\nWrite-Output \"Setting up the Billing Account ID variable...\"\n$billingAccountIdVarName = \"AzureOptimization_BillingAccountID\"\n$billingAccountIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -ErrorAction SilentlyContinue\nif (-not($billingAccountIdVar))\n{\n    New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null\n}\nelse\n{\n    Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingAccountIdVarName -Value $billingAccountId -Encrypted $false | Out-Null\n}\n\nif ($billingProfileId)\n{\n    Write-Output \"Setting up the Billing Profile ID variable...\"\n    $billingProfileIdVarName = \"AzureOptimization_BillingProfileID\"\n    $billingProfileIdVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -ErrorAction SilentlyContinue\n    if (-not($billingProfileIdVar))\n    {\n        New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null\n    }\n    else\n    {\n        Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $billingProfileIdVarName -Value $billingProfileId -Encrypted $false | Out-Null\n    }    \n}\n\n$currencyCode = Read-Host \"Please, enter your consumption currency code (e.g. EUR, USD, etc.)\"\nWrite-Output \"Setting up the consumption currency code variable...\"\n$currencyCodeVarName = \"AzureOptimization_RetailPricesCurrencyCode\"\n$currencyCodeVar = Get-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -ErrorAction SilentlyContinue\nif (-not($currencyCodeVar))\n{\n    New-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null\n}\nelse\n{\n    Set-AzAutomationVariable -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $currencyCodeVarName -Value $currencyCode -Encrypted $false | Out-Null\n}\n\nWrite-Host \"DONE\" -ForegroundColor Green"
  },
  {
    "path": "Setup-DataCollectionRules.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $true)] \n    [String] $DestinationWorkspaceResourceId,\n\n    [Parameter(Mandatory = $false)] \n    [int] $IntervalSeconds = 60,\n\n    [Parameter(Mandatory = $false)]\n    [hashtable] $ResourceTags = @{}\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$ctx = Get-AzContext\nif (-not($ctx)) {\n    Connect-AzAccount -Environment $AzureEnvironment\n    $ctx = Get-AzContext\n}\nelse {\n    if ($ctx.Environment.Name -ne $AzureEnvironment) {\n        Disconnect-AzAccount -ContextName $ctx.Name\n        Connect-AzAccount -Environment $AzureEnvironment\n        $ctx = Get-AzContext\n    }\n}\n\n$lastDeploymentStatePath = \".\\last-deployment-state.json\"\n$deploymentOptions = @{}\n\n$perfCounters = Get-Content -Path \".\\perfcounters.json\" | ConvertFrom-Json \n\nif ((Test-Path -Path $lastDeploymentStatePath))\n{\n    $depOptions = Get-Content -Path $lastDeploymentStatePath | ConvertFrom-Json\n    Write-Host $depOptions -ForegroundColor Green\n    $depOptionsReuse = Read-Host \"Found last deployment options above. Do you want to create Data Collection Rules (DCRs) reusing the last deployment options (Y/N)?\"\n    if (\"Y\", \"y\" -contains $depOptionsReuse)\n    {\n        foreach ($property in $depOptions.PSObject.Properties)\n        {\n            $deploymentOptions[$property.Name] = $property.Value\n        }    \n    }\n}\n\nWrite-Host \"Getting Azure subscriptions...\" -ForegroundColor Yellow\n$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" -and $_.SubscriptionPolicies.QuotaId -notlike \"AAD*\" }\n\nif ($subscriptions.Count -gt 1) {\n\n    $selectedSubscription = -1\n    for ($i = 0; $i -lt $subscriptions.Count; $i++)\n    {\n        if (-not($deploymentOptions[\"SubscriptionId\"]))\n        {\n            Write-Output \"[$i] $($subscriptions[$i].Name)\"    \n        }\n        else\n        {\n            if ($subscriptions[$i].Id -eq $deploymentOptions[\"SubscriptionId\"])\n            {\n                $selectedSubscription = $i\n                break\n            }\n        }\n    }\n    if (-not($deploymentOptions[\"SubscriptionId\"]))\n    {\n        $lastSubscriptionIndex = $subscriptions.Count - 1\n        while ($selectedSubscription -lt 0 -or $selectedSubscription -gt $lastSubscriptionIndex) {\n            Write-Output \"---\"\n            $selectedSubscription = [int] (Read-Host \"Please, select the target subscription for this deployment [0..$lastSubscriptionIndex]\")\n        }    \n    }\n    if ($selectedSubscription -eq -1)\n    {\n        throw \"The selected subscription does not exist. Check if you are logged in with the right Microsoft Entra ID user.\"        \n    }\n}\nelse\n{\n    if ($subscriptions.Count -ne 0)\n    {\n        $selectedSubscription = 0\n    }\n    else\n    {\n        throw \"No valid subscriptions found. Only EA, MCA, PAYG or MSDN subscriptions are supported currently.\"\n    }\n}\n\nif ($subscriptions.Count -eq 0) {\n    throw \"No subscriptions found. Check if you are logged in with the right Microsoft Entra ID account.\"\n}\n\n$subscriptionId = $subscriptions[$selectedSubscription].Id\n\nif ($ctx.Subscription.SubscriptionId -ne $DestinationWorkspaceResourceId.Split('/')[2])\n{\n    $ctx = Set-AzContext -SubscriptionId $DestinationWorkspaceResourceId.Split('/')[2]\n}\n\n$la = Get-AzOperationalInsightsWorkspace -ResourceGroupName $DestinationWorkspaceResourceId.Split('/')[4] -Name $DestinationWorkspaceResourceId.Split('/')[8] -ErrorAction SilentlyContinue\n\nif (-not($la))\n{\n    throw \"The destination workspace ($DestinationWorkspaceResourceId) does not exist. Check if you are logged in with the right Microsoft Entra ID user.\"\n}\n\nif (-not($deploymentOptions[\"NamePrefix\"]))\n{\n    do\n    {\n        $namePrefix = Read-Host \"Please, enter a unique name prefix for the DCRs or existing prefix if updating deployment. If you want instead to individually name all DCRs, just press ENTER\"\n        if (-not($namePrefix))\n        {\n            $namePrefix = \"EmptyNamePrefix\"\n        }\n    } \n    while ($namePrefix.Length -gt 21)\n}\nelse {\n    if ($deploymentOptions[\"NamePrefix\"] -eq \"EmptyNamePrefix\")\n    {\n        $namePrefix = $null\n    }\n    else\n    {\n        $namePrefix = $deploymentOptions[\"NamePrefix\"]            \n    }\n}\n\n$windowsDcrNameTemplate = \"{0}-windows-dcr\"\n$linuxDcrNameTemplate = \"{0}-linux-dcr\"\n\nif (-not($deploymentOptions[\"ResourceGroupName\"]))\n{\n\n    $resourceGroupName = Read-Host \"Please, enter the new or existing Resource Group for this deployment\"\n}\nelse\n{\n    $resourceGroupName = $deploymentOptions[\"ResourceGroupName\"]\n}\n\nif ($ctx.Subscription.SubscriptionId -ne $subscriptionId)\n{\n    $ctx = Set-AzContext -SubscriptionId $subscriptionId\n}\n\n$rg = Get-AzResourceGroup -Name $resourceGroupName\n\nif ([string]::IsNullOrEmpty($namePrefix) -or $namePrefix -eq \"EmptyNamePrefix\") {\n    $windowsDcrName = Read-Host \"Enter the Windows DCR name\"\n    $linuxDcrName = Read-Host \"Enter the Linux DCR name\"\n}\nelse {\n    $windowsDcrName = $windowsDcrNameTemplate -f $namePrefix            \n    $linuxDcrName = $linuxDcrNameTemplate -f $namePrefix\n}\n\nif (-not($deploymentOptions[\"TargetLocation\"]))\n{\n    if (-not($rg.Location)) {\n        Write-Host \"Getting Azure locations...\" -ForegroundColor Green\n        $locations = Get-AzLocation | Where-Object { $_.Providers -contains \"Microsoft.Insights\" } | Sort-Object -Property Location\n        \n        for ($i = 0; $i -lt $locations.Count; $i++) {\n            Write-Output \"[$i] $($locations[$i].location)\"    \n        }\n        $selectedLocation = -1\n        $lastLocationIndex = $locations.Count - 1\n        while ($selectedLocation -lt 0 -or $selectedLocation -gt $lastLocationIndex) {\n            Write-Output \"---\"\n            $selectedLocation = [int] (Read-Host \"Please, select the target location for this deployment [0..$lastLocationIndex]\")\n        }\n        \n        $targetLocation = $locations[$selectedLocation].location    \n    }\n    else {\n        $targetLocation = $rg.Location    \n    }\n}\nelse\n{\n    $targetLocation = $deploymentOptions[\"TargetLocation\"]    \n}\n\n$windowsPerfCounters = @()\nforeach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq \"Windows\"})) {\n    $windowsPerfCounters += $ExecutionContext.InvokeCommand.ExpandString('\"\\\\$($perfCounter.objectName)($($perfCounter.instance))\\\\$($perfCounter.counterName)\"')\n}\n\n$linuxPerfCounters = @()\nforeach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq \"Linux\"})) {\n    $linuxPerfCounters += $ExecutionContext.InvokeCommand.ExpandString('\"\\\\$($perfCounter.objectName)($($perfCounter.instance))\\\\$($perfCounter.counterName)\"')\n}\n\n$windowsDcrBody = @'\n{\n    \"dataSources\": {\n        \"performanceCounters\": [\n            {\n                \"streams\": [\n                    \"Microsoft-Perf\"\n                ],\n                \"samplingFrequencyInSeconds\": $IntervalSeconds,\n                \"counterSpecifiers\": [\n                    $($windowsPerfCounters -join \",\")\n                ],\n                \"name\": \"perfCounterDataSource$IntervalSeconds\"\n            }\n        ]\n    },\n    \"destinations\": {\n        \"logAnalytics\": [\n            {\n                \"workspaceResourceId\": \"$destinationWorkspaceResourceId\",\n                \"workspaceId\": \"$($la.Properties.CustomerId)\",\n                \"name\": \"la--1138206996\"\n            }\n        ]\n    },\n    \"dataFlows\": [\n        {\n            \"streams\": [\n                \"Microsoft-Perf\"\n            ],\n            \"destinations\": [\n                \"la--1138206996\"\n            ]\n        }\n    ]\n}\n'@\n\nWrite-Output \"Creating Windows DCR...\"\n$windowsDcrBody = $ExecutionContext.InvokeCommand.ExpandString($windowsDcrBody) | ConvertFrom-Json\nNew-AzResource -ResourceType \"Microsoft.Insights/dataCollectionRules\" -ResourceGroupName $resourceGroupName -Location $targetLocation -Name $windowsDcrName -PropertyObject $windowsDcrBody -ApiVersion \"2021-04-01\" -Tag $ResourceTags -Kind \"Windows\" -Force | Out-Null\n\n$linuxDcrBody = @'\n{\n    \"dataSources\": {\n        \"performanceCounters\": [\n            {\n                \"streams\": [\n                    \"Microsoft-Perf\"\n                ],\n                \"samplingFrequencyInSeconds\": $IntervalSeconds,\n                \"counterSpecifiers\": [\n                    $($linuxPerfCounters -join \",\")\n                ],\n                \"name\": \"perfCounterDataSource$IntervalSeconds\"\n            }\n        ]\n    },\n    \"destinations\": {\n        \"logAnalytics\": [\n            {\n                \"workspaceResourceId\": \"$destinationWorkspaceResourceId\",\n                \"workspaceId\": \"$($la.Properties.CustomerId)\",\n                \"name\": \"la--1138206996\"\n            }\n        ]\n    },\n    \"dataFlows\": [\n        {\n            \"streams\": [\n                \"Microsoft-Perf\"\n            ],\n            \"destinations\": [\n                \"la--1138206996\"\n            ]\n        }\n    ]\n}\n'@\n\nWrite-Output \"Creating Linux DCR...\"\n$linuxDcrBody = $ExecutionContext.InvokeCommand.ExpandString($linuxDcrBody) | ConvertFrom-Json\nNew-AzResource -ResourceType \"Microsoft.Insights/dataCollectionRules\" -ResourceGroupName $resourceGroupName -Location $targetLocation -Name $linuxDcrName -PropertyObject $linuxDcrBody -ApiVersion \"2021-04-01\" -Tag $ResourceTags -Kind \"Linux\" -Force | Out-Null\n\nWrite-Host -ForegroundColor Green \"Deployment completed successfully\""
  },
  {
    "path": "Setup-LogAnalyticsWorkspaces.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)] \n    [String] $AzureEnvironment = \"AzureCloud\",\n\n    [Parameter(Mandatory = $false)] \n    [String[]] $WorkspaceIds,\n\n    [Parameter(Mandatory = $false)]\n    [switch] $AutoFix,\n\n    [Parameter(Mandatory = $false)] \n    [int] $IntervalSeconds = 60\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$ctx = Get-AzContext\nif (-not($ctx)) {\n    Connect-AzAccount -Environment $AzureEnvironment\n    $ctx = Get-AzContext\n}\nelse {\n    if ($ctx.Environment.Name -ne $AzureEnvironment) {\n        Disconnect-AzAccount -ContextName $ctx.Name\n        Connect-AzAccount -Environment $AzureEnvironment\n        $ctx = Get-AzContext\n    }\n}\n\n$wsIds = foreach ($workspaceId in $WorkspaceIds)\n{\n    \"'$workspaceId'\"\n}\nif ($wsIds)\n{\n    $wsIds = $wsIds -join \",\"\n    $whereWsIds = \" and properties.customerId in ($wsIds)\"\n}\n\n$perfCounters = Get-Content -Path \".\\perfcounters.json\" | ConvertFrom-Json \n\n$ARGPageSize = 1000\n\n$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n\n$argQuery = \"resources | where type =~ 'microsoft.operationalinsights/workspaces'$whereWsIds | order by id\"\n\n$workspaces = (Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions).data\n\nWrite-Output \"Found $($workspaces.Count) workspaces.\"\n\n$laQuery = \"Heartbeat | where TimeGenerated > ago(1d) and ComputerEnvironment == 'Azure' | distinct Computer | summarize AzureComputersCount = count()\"\n\nforeach ($workspace in $workspaces) {\n    $laQueryResults = $null\n    $results = $null\n    $laQueryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspace.properties.customerId -Query $laQuery -Timespan (New-TimeSpan -Days 1) -ErrorAction Continue\n    if ($laQueryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($laQueryResults.Results)\n        Write-Output \"$($workspace.name) ($($workspace.properties.customerId)): $($results.AzureComputersCount) Azure computers connected.\"    \n    }\n    else\n    {\n        Write-Output \"$($workspace.name) ($($workspace.properties.customerId)): could not validate connected computers.\"\n    }\n    if ($results.AzureComputersCount -gt 0)\n    {\n        if ($ctx.Subscription.SubscriptionId -ne $workspace.subscriptionId)\n        {\n            $ctx = Set-AzContext -SubscriptionId $workspace.subscriptionId\n        }\n        $dsWindows = Get-AzOperationalInsightsDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name -Kind WindowsPerformanceCounter\n        foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq \"Windows\"})) {\n            if (-not($dsWindows | Where-Object { $_.Properties.ObjectName -eq $perfCounter.objectName -and $_.Properties.InstanceName -eq $perfCounter.instance `\n                -and $_.Properties.CounterName -eq $perfCounter.counterName}))\n            {\n                Write-Output \"Missing $($perfCounter.objectName)($($perfCounter.instance))\\$($perfCounter.counterName)\"\n                if ($AutoFix)\n                {\n                    Write-Output \"Fixing...\"\n                    $dsName = \"DataSource_WindowsPerformanceCounter_$(New-Guid)\"\n                    New-AzOperationalInsightsWindowsPerformanceCounterDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name `\n                        -Name $dsName -ObjectName $perfCounter.objectName -CounterName $perfCounter.counterName -InstanceName $perfCounter.instance `\n                        -IntervalSeconds $IntervalSeconds -Force | Out-Null\n                }\n            }\n        }\n\n        $missingLinuxCounters = @()\n        $dsLinux = Get-AzOperationalInsightsDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name -Kind LinuxPerformanceObject\n        foreach ($perfCounter in ($perfCounters | Where-Object {$_.osType -eq \"Linux\"})) {\n            if (-not($dsLinux | Where-Object { $_.Properties.ObjectName -eq $perfCounter.objectName -and $_.Properties.InstanceName -eq $perfCounter.instance `\n                -and ($_.Properties.PerformanceCounters | Where-Object { $_.CounterName -eq $perfCounter.counterName }) }))\n            {\n                Write-Output \"Missing $($perfCounter.objectName)($($perfCounter.instance))\\$($perfCounter.counterName)\"\n                if ($AutoFix)\n                {\n                    $missingLinuxCounters += $perfCounter\n                }\n            }\n        }\n\n        if ($AutoFix)\n        {\n            $fixedLinuxCounters = @()\n            $existingLinuxObjects = ($dsLinux | Select-Object -ExpandProperty Properties | Select-Object -Property ObjectName).ObjectName\n            foreach ($linuxObject in $existingLinuxObjects) {\n                $missingObjectCounters = $missingLinuxCounters | Where-Object { $_.objectName -eq $linuxObject }\n                $originalDataSource = $dsLinux | Where-Object { $_.Properties.ObjectName -eq $linuxObject }\n                foreach ($perfCounter in $missingObjectCounters) {\n                    $fixedLinuxCounters += $perfCounter\n                    $newCounterName = New-Object -TypeName Microsoft.Azure.Commands.OperationalInsights.Models.PerformanceCounterIdentifier -Property @{CounterName = $perfCounter.counterName}\n                    $originalDataSource.Properties.PerformanceCounters.Add($newCounterName)\n                }\n                if ($missingObjectCounters)\n                {\n                    Write-Output \"Fixing $linuxObject object...\"\n                    Set-AzOperationalInsightsDataSource -DataSource $originalDataSource | Out-Null\n                }\n            }\n            $missingObjects = ($missingLinuxCounters | Select-Object -Property objectName -Unique).objectName\n            $fixedObjects = ($fixedLinuxCounters | Select-Object -Property objectName -Unique).objectName\n            $missingObjects = $missingObjects | Where-Object { -not($_ -in $fixedObjects) }\n            foreach ($linuxObject in $missingObjects) {\n                $missingObjectCounters = $missingLinuxCounters | Where-Object { $_.objectName -eq $linuxObject }\n                $missingInstance = ($missingObjectCounters | Select-Object -Property instance -Unique -First 1).instance\n                $missingCounterNames = ($missingObjectCounters).counterName\n    \n                Write-Output \"Adding $linuxObject object...\"\n                New-AzOperationalInsightsLinuxPerformanceObjectDataSource -ResourceGroupName $workspace.resourceGroup -WorkspaceName $workspace.name `\n                    -Name \"DataSource_LinuxPerformanceObject_$(New-Guid)\" -ObjectName $linuxObject -InstanceName $missingInstance -IntervalSeconds $IntervalSeconds `\n                    -CounterNames $missingCounterNames -Force | Out-Null\n            }    \n        }\n    }\n}"
  },
  {
    "path": "Suppress-Recommendation.ps1",
    "content": "param(\n    [Parameter(Mandatory = $true)] \n    [String] $RecommendationId\n)\n\n$ErrorActionPreference = \"Stop\"\n\nfunction Test-IsGuid\n{\n    [OutputType([bool])]\n    param\n    (\n        [Parameter(Mandatory = $true)]\n        [string]$ObjectGuid\n    )\n\n    # Define verification regex\n    [regex]$guidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'\n\n    # Check guid against regex\n    return $ObjectGuid -match $guidRegex\n}\n\nif (-not(Test-IsGuid -ObjectGuid $RecommendationId))\n{\n    Write-Host \"The provided recommendation Id is invalid. Must be a valid GUID.\" -ForegroundColor Red\n    Exit\n}\n\n$databaseConnectionSettingsPath = \".\\database-connection-settings.json\"\n$dbConnectionSettings = @{}\n\nif (Test-Path -Path $databaseConnectionSettingsPath)\n{\n    $dbSettings = Get-Content -Path $databaseConnectionSettingsPath | ConvertFrom-Json\n    Write-Host $dbSettings -ForegroundColor Green\n    $dbSettingsReuse = Read-Host \"Found existing database connection settings. Do you want to reuse them (Y/N)?\"\n    if (\"Y\", \"y\" -contains $dbSettingsReuse)\n    {\n        foreach ($property in $dbSettings.PSObject.Properties)\n        {\n            $dbConnectionSettings[$property.Name] = $property.Value\n        }    \n    }\n}\n\nif (-not($dbConnectionSettings[\"DatabaseServer\"]))\n{\n    $databaseServer = Read-Host \"Please, enter the AOE Azure SQL server hostname (e.g., xpto.database.windows.net)\"\n    $dbConnectionSettings[\"DatabaseServer\"] = $databaseServer\n}\nelse\n{\n    $databaseServer = $dbConnectionSettings[\"DatabaseServer\"]\n}\n\nif (-not($dbConnectionSettings[\"DatabaseName\"]))\n{\n    $databaseName = Read-Host \"Please, enter the AOE Azure SQL Database name (e.g., azureoptimization)\"\n    $dbConnectionSettings[\"DatabaseName\"] = $databaseName\n}\nelse\n{\n    $databaseName = $dbConnectionSettings[\"DatabaseName\"]\n}\n\nif (-not($dbConnectionSettings[\"DatabaseUser\"]))\n{\n    $databaseUser = Read-Host \"Please, enter the AOE database user name\"\n    $dbConnectionSettings[\"DatabaseUser\"] = $databaseUser\n}\nelse\n{\n    $databaseUser = $dbConnectionSettings[\"DatabaseUser\"]\n}\n\n$sqlPass = Read-Host \"Please, input the password for the $databaseUser SQL user\" -AsSecureString\n$sqlPassPlain = (New-Object PSCredential \"user\", $sqlPass).GetNetworkCredential().Password\n$sqlPassPlain = $sqlPassPlain.Replace(\"'\", \"''\")\n\n$SqlTimeout = 120\n$recommendationsTable = \"Recommendations\"\n$suppressionsTable = \"Filters\"\n\nWrite-Host \"Opening connection to the database...\" -ForegroundColor Green\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$databaseServer,1433;Database=$databaseName;User ID=$databaseUser;Password='$sqlPassPlain';Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$recommendationsTable] WHERE RecommendationId = '$RecommendationId'\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Host \"Failed to contact SQL at try $tries.\" -ForegroundColor Yellow\n        Write-Host $Error[0] -ForegroundColor Yellow\n        Write-Output \"Waiting $($tries * 20) seconds...\"\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$Conn.Close()    \n$Conn.Dispose()            \n\nif (-not($controlRows.RecommendationId))\n{\n    Write-Host \"The provided recommendation Id was not found. Please, try again with a valid GUID.\" -ForegroundColor Red\n    Exit\n}\n\nWrite-Host \"You are suppressing the recommendation with the below details\" -ForegroundColor Green\nWrite-Host \"Recommendation: $($controlRows.RecommendationDescription)\" -ForegroundColor Blue\nWrite-Host \"Recommendation sub-type id: $($controlRows.RecommendationSubTypeId)\" -ForegroundColor Blue\nWrite-Host \"Category: $($controlRows.Category)\" -ForegroundColor Blue\nWrite-Host \"Instance Name: $($controlRows.InstanceName)\" -ForegroundColor Blue\nWrite-Host \"Resource Group: $($controlRows.ResourceGroup)\" -ForegroundColor Blue\nWrite-Host \"Subscription Id: $($controlRows.SubscriptionGuid)\" -ForegroundColor Blue\nWrite-Host \"Please, choose the suppression type\" -ForegroundColor Green\nWrite-Host \"[E]xclude - this recommendation type will be completely excluded from the engine and will no longer be generated for any resource\" -ForegroundColor Green\nWrite-Host \"[D]ismiss - this recommendation will be dismissed for the scope to be chosen next (instance, resource group or subscription)\" -ForegroundColor Green\nWrite-Host \"[S]nooze - this recommendation will be postponed for the duration (in days) and scope to be chosen next (instance, resource group or subscription)\" -ForegroundColor Green\nWrite-Host \"[C]ancel - no action will be taken\" -ForegroundColor Green\n$suppOption = Read-Host \"Enter your choice (E, D, S or C)\"\n\nif (\"E\", \"e\" -contains $suppOption)\n{\n    $suppressionType = \"Exclude\"\n}\nelseif (\"D\", \"d\" -contains $suppOption)\n{\n    $suppressionType = \"Dismiss\"\n}\nelseif (\"S\", \"s\" -contains $suppOption)\n{\n    $suppressionType = \"Snooze\"\n}\nelse\n{\n    Write-Host \"Cancelling.. No action will be taken.\" -ForegroundColor Green    \n    Exit\n}\n\nif ($suppressionType -in (\"Dismiss\", \"Snooze\"))\n{\n    Write-Host \"Please, choose the scope for the suppression\" -ForegroundColor Green\n    Write-Host \"[S]ubscription ($($controlRows.SubscriptionGuid))\" -ForegroundColor Green\n    Write-Host \"[R]esource Group ($($controlRows.ResourceGroup))\" -ForegroundColor Green\n    Write-Host \"[I]nstance ($($controlRows.InstanceName))\" -ForegroundColor Green\n    $scopeOption = Read-Host \"Enter your choice (S, R, or I)\"\n\n    if (\"S\", \"s\" -contains $scopeOption)\n    {\n        $scope = $controlRows.SubscriptionGuid\n    }\n    elseif (\"R\", \"r\" -contains $scopeOption)\n    {\n        $scope = $controlRows.ResourceGroup\n    }\n    elseif (\"I\", \"i\" -contains $scopeOption)\n    {\n        $scope = $controlRows.InstanceId\n    }\n    else\n    {\n        Write-Host \"Wrong input. No action will be taken.\" -ForegroundColor Red\n        Exit\n    }\n}\n\n$snoozeDays = 0\nif ($suppressionType -eq \"Snooze\")\n{\n    Write-Host \"Please, enter the number of days the recommendation will be snoozed\" -ForegroundColor Green\n    $snoozeDays = Read-Host \"Number of days (min. 14)\"\n    if (-not($snoozeDays -ge 14))\n    {\n        Write-Host \"Wrong snooze days. No action will be taken.\" -ForegroundColor Red\n        Exit\n    }\n}\n\n$author = Read-Host \"Please enter your name\"\n$notes = Read-Host \"Please enter a reason for this suppression\"\n\nWrite-Host \"You are about to suppress this recommendation\" -ForegroundColor Yellow\nWrite-Host \"Recommendation: $($controlRows.RecommendationDescription)\" -ForegroundColor Blue\nWrite-Host \"Suppression type: $suppressionType\" -ForegroundColor Blue\nif ($suppressionType -in (\"Dismiss\", \"Snooze\"))\n{\n    Write-Host \"Scope: $scope\" -ForegroundColor Blue    \n}\nif ($suppressionType -eq \"Snooze\")\n{\n    Write-Host \"Snooze days: $snoozeDays\" -ForegroundColor Blue    \n}\nWrite-Host \"Author: $author\" -ForegroundColor Blue\nWrite-Host \"Reason: $notes\" -ForegroundColor Blue\n$continueInput = Read-Host \"Do you want to continue (Y/N)?\"\nif (\"Y\", \"y\" -contains $continueInput) \n{\n    if ($scope)\n    {\n        $scope = \"'$scope'\"\n    }\n    else\n    {\n        $scope = \"NULL\"    \n    }\n\n    if ($snoozeDays -ge 14)\n    {\n        $now = (Get-Date).ToUniversalTime()\n        $endDate = \"'$($now.Add($snoozeDays).ToString(\"yyyy-MM-ddTHH:mm:00Z\"))'\"\n    }\n    else {\n        $endDate = \"NULL\"\n    }\n\n    $sqlStatement = \"INSERT INTO [$suppressionsTable] VALUES (NEWID(), '$($controlRows.RecommendationSubTypeId)', '$suppressionType', $scope, GETDATE(), $endDate, '$author', '$notes', 1)\"\n\n    $Conn2 = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$databaseServer,1433;Database=$databaseName;User ID=$databaseUser;Password='$sqlPassPlain';Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n    $Conn2.Open() \n    \n    $Cmd=new-object system.Data.SqlClient.SqlCommand\n    $Cmd.Connection = $Conn2\n    $Cmd.CommandText = $sqlStatement\n    $Cmd.CommandTimeout=120 \n    try\n    {\n        $Cmd.ExecuteReader()\n    }\n    catch\n    {\n        Write-Output \"Failed statement: $sqlStatement\"\n        throw\n    }\n    \n    $Conn2.Close()                \n\n    Write-Host \"Suppression sucessfully added.\" -ForegroundColor Green\n}\nelse\n{\n    Write-Host \"No action was taken.\" -ForegroundColor Green\n}\n\n$dbConnectionSettings | ConvertTo-Json | Out-File -FilePath $databaseConnectionSettingsPath -Force"
  },
  {
    "path": "azuredeploy-nested.bicep",
    "content": "param projectLocation string\nparam templateLocation string\n\nparam storageAccountName string\nparam automationAccountName string\nparam sqlServerName string\nparam sqlDatabaseName string\nparam logAnalyticsReuse bool\nparam logAnalyticsWorkspaceName string\nparam logAnalyticsWorkspaceRG string\nparam logAnalyticsRetentionDays int\nparam sqlBackupRetentionDays int\nparam sqlAdminLogin string\n\n@secure()\nparam sqlAdminPassword string\nparam cloudEnvironment string\nparam authenticationOption string\nparam baseTime string\nparam resourceTags object\nparam contributorRoleAssignmentGuid string\n\nparam argDiskExportJobId string = newGuid()\nparam argVhdExportJobId string = newGuid()\nparam argVmExportJobId string = newGuid()\nparam argVmssExportJobId string = newGuid()\nparam argAvailSetExportJobId string = newGuid()\nparam advisorExportJobId string = newGuid()\nparam consumptionExportJobId string = newGuid()\nparam aadObjectsExportJobId string = newGuid()\nparam argLoadBalancersExportJobId string = newGuid()\nparam argAppGWsExportJobId string = newGuid()\nparam rbacExportJobId string = newGuid()\nparam argResContainersExportJobId string = newGuid()\nparam argNICExportJobId string = newGuid()\nparam argNSGExportJobId string = newGuid()\nparam argPublicIPExportJobId string = newGuid()\nparam argVNetExportJobId string = newGuid()\nparam argSqlDbExportJobId string = newGuid()\nparam policyStateExportJobId string = newGuid()\nparam monitorVmssCpuMaxExportJobId string = newGuid()\nparam monitorVmssCpuAvgExportJobId string = newGuid()\nparam monitorVmssMemoryMinExportJobId string = newGuid()\nparam monitorSqlDbDtuMaxExportJobId string = newGuid()\nparam monitorSqlDbDtuAvgExportJobId string = newGuid()\nparam monitorAppServiceCpuMaxExportJobId string = newGuid()\nparam monitorAppServiceCpuAvgExportJobId string = newGuid()\nparam monitorAppServiceMemoryMaxExportJobId string = newGuid()\nparam monitorAppServiceMemoryAvgExportJobId string = newGuid()\nparam monitorDiskIOPSAvgExportJobId string = newGuid()\nparam monitorDiskMBPsAvgExportJobId string = newGuid()\nparam argAppServicePlanExportJobId string = newGuid()\nparam pricesheetExportJobId string = newGuid()\nparam reservationPricesExportJobId string = newGuid()\nparam reservationUsageExportJobId string = newGuid()\nparam savingsPlansUsageExportJobId string = newGuid()\nparam argDiskIngestJobId string = newGuid()\nparam argVhdIngestJobId string = newGuid()\nparam argVmIngestJobId string = newGuid()\nparam argVmssIngestJobId string = newGuid()\nparam argAvailSetIngestJobId string = newGuid()\nparam advisorIngestJobId string = newGuid()\nparam remediationLogsIngestJobId string = newGuid()\nparam consumptionIngestJobId string = newGuid()\nparam aadObjectsIngestJobId string = newGuid()\nparam argLoadBalancersIngestJobId string = newGuid()\nparam argAppGWsIngestJobId string = newGuid()\nparam argResContainersIngestJobId string = newGuid()\nparam rbacIngestJobId string = newGuid()\nparam argNICIngestJobId string = newGuid()\nparam argNSGIngestJobId string = newGuid()\nparam argPublicIPIngestJobId string = newGuid()\nparam argVNetIngestJobId string = newGuid()\nparam argSqlDbIngestJobId string = newGuid()\nparam policyStateIngestJobId string = newGuid()\nparam monitorIngestJobId string = newGuid()\nparam argAppServicePlanIngestJobId string = newGuid()\nparam pricesheetIngestJobId string = newGuid()\nparam reservationPricesIngestJobId string = newGuid()\nparam reservationUsageIngestJobId string = newGuid()\nparam savingsPlansUsageIngestJobId string = newGuid()\nparam unattachedDisksRecommendationJobId string = newGuid()\nparam advisorCostAugmentedRecommendationJobId string = newGuid()\nparam advisorAsIsRecommendationJobId string = newGuid()\nparam vmsHaRecommendationJobId string = newGuid()\nparam vmOptimizationsRecommendationJobId string = newGuid()\nparam aadExpiringCredsRecommendationJobId string = newGuid()\nparam unusedLoadBalancersRecommendationJobId string = newGuid()\nparam unusedAppGWsRecommendationJobId string = newGuid()\nparam armOptimizationsRecommendationJobId string = newGuid()\nparam vnetOptimizationsRecommendationJobId string = newGuid()\nparam vmssOptimizationsRecommendationJobId string = newGuid()\nparam sqldbOptimizationsRecommendationJobId string = newGuid()\nparam storageOptimizationsRecommendationJobId string = newGuid()\nparam appServiceOptimizationsRecommendationJobId string = newGuid()\nparam diskOptimizationsRecommendationJobId string = newGuid()\nparam recommendationsIngestJobId string = newGuid()\nparam recommendationsLogAnalyticsIngestJobId string = newGuid()\nparam suppressionsLogAnalyticsIngestJobId string = newGuid()\nparam recommendationsCleanUpJobId string = newGuid()\n\nparam roleContributor string = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c'\n\nvar advisorExportsRunbookName = 'Export-AdvisorRecommendationsToBlobStorage'\nvar argVmExportsRunbookName = 'Export-ARGVirtualMachinesPropertiesToBlobStorage'\nvar argVmssExportsRunbookName = 'Export-ARGVMSSPropertiesToBlobStorage'\nvar argDisksExportsRunbookName = 'Export-ARGManagedDisksPropertiesToBlobStorage'\nvar argVhdExportsRunbookName = 'Export-ARGUnmanagedDisksPropertiesToBlobStorage'\nvar argAvailSetExportsRunbookName = 'Export-ARGAvailabilitySetPropertiesToBlobStorage'\nvar consumptionExportsRunbookName = 'Export-ConsumptionToBlobStorage'\nvar aadObjectsExportsRunbookName = 'Export-AADObjectsToBlobStorage'\nvar argLoadBalancersExportsRunbookName = 'Export-ARGLoadBalancerPropertiesToBlobStorage'\nvar argAppGWsExportsRunbookName = 'Export-ARGAppGatewayPropertiesToBlobStorage'\nvar argResContainersExportsRunbookName = 'Export-ARGResourceContainersPropertiesToBlobStorage'\nvar rbacExportsRunbookName = 'Export-RBACAssignmentsToBlobStorage'\nvar argNICExportsRunbookName = 'Export-ARGNICPropertiesToBlobStorage'\nvar argNSGExportsRunbookName = 'Export-ARGNSGPropertiesToBlobStorage'\nvar argVNetExportsRunbookName = 'Export-ARGVNetPropertiesToBlobStorage'\nvar argPublicIpExportsRunbookName = 'Export-ARGPublicIpPropertiesToBlobStorage'\nvar argSqlDbExportsRunbookName = 'Export-ARGSqlDatabasePropertiesToBlobStorage'\nvar policyStateExportsRunbookName = 'Export-PolicyComplianceToBlobStorage'\nvar monitorExportsRunbookName = 'Export-AzMonitorMetricsToBlobStorage'\nvar argAppServicePlanExportsRunbookName = 'Export-ARGAppServicePlanPropertiesToBlobStorage'\nvar reservationsExportsRunbookName = 'Export-ReservationsUsageToBlobStorage'\nvar reservationsPriceExportsRunbookName = 'Export-ReservationsPriceToBlobStorage'\nvar priceSheetExportsRunbookName = 'Export-PriceSheetToBlobStorage'\nvar savingsPlansExportsRunbookName = 'Export-SavingsPlansUsageToBlobStorage'\nvar advisorExportsScheduleName = 'AzureOptimization_ExportAdvisorWeekly'\nvar argExportsScheduleName = 'AzureOptimization_ExportARGDaily'\nvar consumptionExportsScheduleName = 'AzureOptimization_ExportConsumptionDaily'\nvar aadObjectsExportsScheduleName = 'AzureOptimization_ExportAADObjectsDaily'\nvar rbacExportsScheduleName = 'AzureOptimization_ExportRBACDaily'\nvar policyStateExportsScheduleName = 'AzureOptimization_ExportPolicyStateDaily'\nvar monitorVmssCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuMaxHourly'\nvar monitorVmssCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorVmssCpuAvgHourly'\nvar monitorVmssMemoryMinExportsScheduleName = 'AzureOptimization_ExportMonitorVmssMemoryMinHourly'\nvar monitorSqlDbDtuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuMaxHourly'\nvar monitorSqlDbDtuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorSqlDbDtuAvgHourly'\nvar monitorAppServiceCpuMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuMaxHourly'\nvar monitorAppServiceCpuAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceCpuAvgHourly'\nvar monitorAppServiceMemoryMaxExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryMaxHourly'\nvar monitorAppServiceMemoryAvgExportsScheduleName = 'AzureOptimization_ExportMonitorAppServiceMemoryAvgHourly'\nvar monitorDiskIOPSAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskIOPSHourly'\nvar monitorDiskMBPsAvgExportsScheduleName = 'AzureOptimization_ExportMonitorDiskMBPsHourly'\nvar priceExportsScheduleName = 'AzureOptimization_ExportPricesWeekly'\nvar reservationsUsageExportsScheduleName = 'AzureOptimization_ExportReservationsDaily'\nvar savingsPlansUsageExportsScheduleName = 'AzureOptimization_ExportSavingsPlansDaily'\nvar csvExportsSchedules = [\n  {\n    exportSchedule: argExportsScheduleName\n    exportDescription: 'Daily Azure Resource Graph exports'\n    exportTimeOffset: 'PT1H05M'\n    exportFrequency: 'Day'\n  }\n  {\n    exportSchedule: advisorExportsScheduleName\n    exportDescription: 'Weekly Azure Advisor exports'\n    exportTimeOffset: 'PT1H15M'\n    exportFrequency: 'Week'\n  }\n  {\n    exportSchedule: consumptionExportsScheduleName\n    exportDescription: 'Daily Azure Consumption exports'\n    exportTimeOffset: 'PT1H'\n    exportFrequency: 'Day'\n  }\n  {\n    exportSchedule: aadObjectsExportsScheduleName\n    exportDescription: 'Daily Microsoft Entra Objects exports'\n    exportTimeOffset: 'PT1H'\n    exportFrequency: 'Day'\n  }\n  {\n    exportSchedule: rbacExportsScheduleName\n    exportDescription: 'Daily Azure RBAC exports'\n    exportTimeOffset: 'PT1H02M'\n    exportFrequency: 'Day'\n  }\n  {\n    exportSchedule: policyStateExportsScheduleName\n    exportDescription: 'Daily Azure Policy State exports'\n    exportTimeOffset: 'PT1H'\n    exportFrequency: 'Day'\n  }\n  {\n    exportSchedule: monitorVmssCpuAvgExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Avg.)'\n    exportTimeOffset: 'PT1H15M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorVmssCpuMaxExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Percentage CPU (Max.)'\n    exportTimeOffset: 'PT1H15M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorVmssMemoryMinExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for VMSS Available Memory (Min.)'\n    exportTimeOffset: 'PT1H15M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorSqlDbDtuMaxExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Max.)'\n    exportTimeOffset: 'PT1H15M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorSqlDbDtuAvgExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for SQL Database Percentage DTU (Avg.)'\n    exportTimeOffset: 'PT1H16M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorAppServiceCpuAvgExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Avg.)'\n    exportTimeOffset: 'PT1H16M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorAppServiceCpuMaxExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage CPU (Max.)'\n    exportTimeOffset: 'PT1H16M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Avg.)'\n    exportTimeOffset: 'PT1H16M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for App Service Percentage RAM (Max.)'\n    exportTimeOffset: 'PT1H17M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorDiskIOPSAvgExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for Disk IOPS (Avg.)'\n    exportTimeOffset: 'PT1H17M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: monitorDiskMBPsAvgExportsScheduleName\n    exportDescription: 'Hourly Azure Monitor metrics exports for Disk MBPs (Avg.)'\n    exportTimeOffset: 'PT1H17M'\n    exportFrequency: 'Hour'\n  }\n  {\n    exportSchedule: priceExportsScheduleName\n    exportDescription: 'Weekly Pricesheet and Reservation Prices exports'\n    exportTimeOffset: 'PT1H35M'\n    exportFrequency: 'Week'\n  }\n  {\n    exportSchedule: reservationsUsageExportsScheduleName\n    exportDescription: 'Daily Reservation Usage exports'\n    exportTimeOffset: 'PT2H'\n    exportFrequency: 'Day'\n  }\n  {\n    exportSchedule: savingsPlansUsageExportsScheduleName\n    exportDescription: 'Daily Savings Plans Usage exports'\n    exportTimeOffset: 'PT2H05M'\n    exportFrequency: 'Day'\n  }\n]\nvar csvExports = [\n  {\n    runbookName: advisorExportsRunbookName\n    isOneToMany: false\n    containerName: 'advisorexports'\n    variableName: 'AzureOptimization_AdvisorContainer'\n    variableDescription: 'The Storage Account container where Azure Advisor exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestAdvisorWeekly'\n    ingestDescription: 'Weekly Azure Advisor recommendations ingests'\n    ingestTimeOffset: 'PT1H45M'\n    ingestFrequency: 'Week'\n    ingestJobId: advisorIngestJobId\n    exportSchedule: advisorExportsScheduleName\n    exportJobId: advisorExportJobId\n  }\n  {\n    runbookName: argVmExportsRunbookName\n    isOneToMany: false\n    containerName: 'argvmexports'\n    variableName: 'AzureOptimization_ARGVMContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Virtual Machine exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGVMsDaily'\n    ingestDescription: 'Daily Azure Resource Graph Virtual Machines ingests'\n    ingestTimeOffset: 'PT1H30M'\n    ingestFrequency: 'Day'\n    ingestJobId: argVmIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argVmExportJobId\n  }\n  {\n    runbookName: argVmssExportsRunbookName\n    isOneToMany: false\n    containerName: 'argvmssexports'\n    variableName: 'AzureOptimization_ARGVMSSContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph VMSS exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGVMSSDaily'\n    ingestDescription: 'Daily Azure Resource Graph VMSS ingests'\n    ingestTimeOffset: 'PT1H30M'\n    ingestFrequency: 'Day'\n    ingestJobId: argVmssIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argVmssExportJobId\n  }\n  {\n    runbookName: argDisksExportsRunbookName\n    isOneToMany: false\n    containerName: 'argdiskexports'\n    variableName: 'AzureOptimization_ARGDiskContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Managed Disks exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGDisksDaily'\n    ingestDescription: 'Daily Azure Resource Graph Managed Disks ingests'\n    ingestTimeOffset: 'PT1H30M'\n    ingestFrequency: 'Day'\n    ingestJobId: argDiskIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argDiskExportJobId\n  }\n  {\n    runbookName: argVhdExportsRunbookName\n    isOneToMany: false\n    containerName: 'argvhdexports'\n    variableName: 'AzureOptimization_ARGVhdContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Unmanaged Disks exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGVHDsDaily'\n    ingestDescription: 'Daily Azure Resource Graph Unmanaged Disks ingests'\n    ingestTimeOffset: 'PT1H30M'\n    ingestFrequency: 'Day'\n    ingestJobId: argVhdIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argVhdExportJobId\n  }\n  {\n    runbookName: argAvailSetExportsRunbookName\n    isOneToMany: false\n    containerName: 'argavailsetexports'\n    variableName: 'AzureOptimization_ARGAvailabilitySetContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Availability Set exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGAvailSetsDaily'\n    ingestDescription: 'Daily Azure Resource Graph Availability Sets ingests'\n    ingestTimeOffset: 'PT1H31M'\n    ingestFrequency: 'Day'\n    ingestJobId: argAvailSetIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argAvailSetExportJobId\n  }\n  {\n    runbookName: consumptionExportsRunbookName\n    isOneToMany: false\n    containerName: 'consumptionexports'\n    variableName: 'AzureOptimization_ConsumptionContainer'\n    variableDescription: 'The Storage Account container where Azure Consumption exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestConsumptionDaily'\n    ingestDescription: 'Daily Azure Consumption ingests'\n    ingestTimeOffset: 'PT2H'\n    ingestFrequency: 'Day'\n    ingestJobId: consumptionIngestJobId\n    exportSchedule: consumptionExportsScheduleName\n    exportJobId: consumptionExportJobId\n  }\n  {\n    runbookName: aadObjectsExportsRunbookName\n    isOneToMany: false\n    containerName: 'aadobjectsexports'\n    variableName: 'AzureOptimization_AADObjectsContainer'\n    variableDescription: 'The Storage Account container where Microsoft Entra Objects exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestAADObjectsDaily'\n    ingestDescription: 'Daily Microsoft Entra Objects ingests'\n    ingestTimeOffset: 'PT2H'\n    ingestFrequency: 'Day'\n    ingestJobId: aadObjectsIngestJobId\n    exportSchedule: aadObjectsExportsScheduleName\n    exportJobId: aadObjectsExportJobId\n  }\n  {\n    runbookName: argLoadBalancersExportsRunbookName\n    isOneToMany: false\n    containerName: 'arglbexports'\n    variableName: 'AzureOptimization_ARGLoadBalancerContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Load Balancer exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGLoadBalancersDaily'\n    ingestDescription: 'Daily Azure Resource Graph Load Balancers ingests'\n    ingestTimeOffset: 'PT1H31M'\n    ingestFrequency: 'Day'\n    ingestJobId: argLoadBalancersIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argLoadBalancersExportJobId\n  }\n  {\n    runbookName: argAppGWsExportsRunbookName\n    isOneToMany: false\n    containerName: 'argappgwexports'\n    variableName: 'AzureOptimization_ARGAppGatewayContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Application Gateway exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGAppGWsDaily'\n    ingestDescription: 'Daily Azure Resource Graph Application Gateways ingests'\n    ingestTimeOffset: 'PT1H31M'\n    ingestFrequency: 'Day'\n    ingestJobId: argAppGWsIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argAppGWsExportJobId\n  }\n  {\n    runbookName: argResContainersExportsRunbookName\n    isOneToMany: false\n    containerName: 'argrescontainersexports'\n    variableName: 'AzureOptimization_ARGResourceContainersContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Resource Containers exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGResourceContainersDaily'\n    ingestDescription: 'Daily Azure Resource Graph Resource Containers ingests'\n    ingestTimeOffset: 'PT1H32M'\n    ingestFrequency: 'Day'\n    ingestJobId: argResContainersIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argResContainersExportJobId\n  }\n  {\n    runbookName: rbacExportsRunbookName\n    isOneToMany: false\n    containerName: 'rbacexports'\n    variableName: 'AzureOptimization_RBACAssignmentsContainer'\n    variableDescription: 'The Storage Account container where RBAC Assignments exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestRBACDaily'\n    ingestDescription: 'Daily Azure RBAC ingests'\n    ingestTimeOffset: 'PT1H32M'\n    ingestFrequency: 'Day'\n    ingestJobId: rbacIngestJobId\n    exportSchedule: rbacExportsScheduleName\n    exportJobId: rbacExportJobId\n  }\n  {\n    runbookName: argNICExportsRunbookName\n    isOneToMany: false\n    containerName: 'argnicexports'\n    variableName: 'AzureOptimization_ARGNICContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph NIC exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGNICsDaily'\n    ingestDescription: 'Daily Azure Resource Graph NIC ingests'\n    ingestTimeOffset: 'PT1H32M'\n    ingestFrequency: 'Day'\n    ingestJobId: argNICIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argNICExportJobId\n  }\n  {\n    runbookName: argNSGExportsRunbookName\n    isOneToMany: false\n    containerName: 'argnsgexports'\n    variableName: 'AzureOptimization_ARGNSGContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph NSG exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGNSGsDaily'\n    ingestDescription: 'Daily Azure Resource Graph NSG ingests'\n    ingestTimeOffset: 'PT1H32M'\n    ingestFrequency: 'Day'\n    ingestJobId: argNSGIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argNSGExportJobId\n  }\n  {\n    runbookName: argVNetExportsRunbookName\n    isOneToMany: false\n    containerName: 'argvnetexports'\n    variableName: 'AzureOptimization_ARGVNetContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph VNet exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGVNetsDaily'\n    ingestDescription: 'Daily Azure Resource Graph Virtual Network ingests'\n    ingestTimeOffset: 'PT1H33M'\n    ingestFrequency: 'Day'\n    ingestJobId: argVNetIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argVNetExportJobId\n  }\n  {\n    runbookName: argPublicIpExportsRunbookName\n    isOneToMany: false\n    containerName: 'argpublicipexports'\n    variableName: 'AzureOptimization_ARGPublicIpContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph Public IP exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGPublicIPsDaily'\n    ingestDescription: 'Daily Azure Resource Graph Public IP ingests'\n    ingestTimeOffset: 'PT1H33M'\n    ingestFrequency: 'Day'\n    ingestJobId: argPublicIPIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argPublicIPExportJobId\n  }\n  {\n    runbookName: argSqlDbExportsRunbookName\n    isOneToMany: false\n    containerName: 'argsqldbexports'\n    variableName: 'AzureOptimization_ARGSqlDatabaseContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph SQL DB exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGSqlDbDaily'\n    ingestDescription: 'Daily Azure Resource Graph SQL DB ingests'\n    ingestTimeOffset: 'PT1H33M'\n    ingestFrequency: 'Day'\n    ingestJobId: argSqlDbIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argSqlDbExportJobId\n  }\n  {\n    runbookName: policyStateExportsRunbookName\n    isOneToMany: false\n    containerName: 'policystateexports'\n    variableName: 'AzureOptimization_PolicyStatesContainer'\n    variableDescription: 'The Storage Account container where Azure Policy State exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestPolicyStateDaily'\n    ingestDescription: 'Daily Azure Policy State ingests'\n    ingestTimeOffset: 'PT1H33M'\n    ingestFrequency: 'Day'\n    ingestJobId: policyStateIngestJobId\n    exportSchedule: policyStateExportsScheduleName\n    exportJobId: policyStateExportJobId\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    isOneToMany: true\n    containerName: 'azmonitorexports'\n    variableName: 'AzureOptimization_AzMonitorContainer'\n    variableDescription: 'The Storage Account container where Azure Monitor metrics exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestAzMonitorMetricsHourly'\n    ingestDescription: 'Hourly Azure Monitor metrics ingests'\n    ingestTimeOffset: 'PT2H'\n    ingestFrequency: 'Hour'\n    ingestJobId: monitorIngestJobId\n    exportSchedule: null\n    exportJobId: 'dummy'\n  }\n  {\n    runbookName: argAppServicePlanExportsRunbookName\n    isOneToMany: false\n    containerName: 'argappserviceplanexports'\n    variableName: 'AzureOptimization_ARGAppServicePlanContainer'\n    variableDescription: 'The Storage Account container where Azure Resource Graph App Service Plan exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestARGAppServicePlanDaily'\n    ingestDescription: 'Daily Azure Resource Graph App Service Plan ingests'\n    ingestTimeOffset: 'PT1H34M'\n    ingestFrequency: 'Day'\n    ingestJobId: argAppServicePlanIngestJobId\n    exportSchedule: argExportsScheduleName\n    exportJobId: argAppServicePlanExportJobId\n  }\n  {\n    runbookName: priceSheetExportsRunbookName\n    isOneToMany: false\n    containerName: 'pricesheetexports'\n    variableName: 'AzureOptimization_PriceSheetContainer'\n    variableDescription: 'The Storage Account container where Pricesheet exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestPricesheetWeekly'\n    ingestDescription: 'Weekly Pricesheet ingests'\n    ingestTimeOffset: 'PT2H'\n    ingestFrequency: 'Week'\n    ingestJobId: pricesheetIngestJobId\n    exportSchedule: priceExportsScheduleName\n    exportJobId: pricesheetExportJobId\n  }\n  {\n    runbookName: reservationsPriceExportsRunbookName\n    isOneToMany: false\n    containerName: 'reservationspriceexports'\n    variableName: 'AzureOptimization_ReservationsPriceContainer'\n    variableDescription: 'The Storage Account container where Reservations Prices exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestReservationsPriceWeekly'\n    ingestDescription: 'Weekly Reservations Prices ingests'\n    ingestTimeOffset: 'PT2H'\n    ingestFrequency: 'Week'\n    ingestJobId: reservationPricesIngestJobId\n    exportSchedule: priceExportsScheduleName\n    exportJobId: reservationPricesExportJobId\n  }\n  {\n    runbookName: reservationsExportsRunbookName\n    isOneToMany: false\n    containerName: 'reservationsexports'\n    variableName: 'AzureOptimization_ReservationsContainer'\n    variableDescription: 'The Storage Account container where Reservations Usage exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestReservationsUsageDaily'\n    ingestDescription: 'Daily Reservations Usage ingests'\n    ingestTimeOffset: 'PT2H30M'\n    ingestFrequency: 'Day'\n    ingestJobId: reservationUsageIngestJobId\n    exportSchedule: reservationsUsageExportsScheduleName\n    exportJobId: reservationUsageExportJobId\n  }\n  {\n    runbookName: savingsPlansExportsRunbookName\n    isOneToMany: false\n    containerName: 'savingsplansexports'\n    variableName: 'AzureOptimization_SavingsPlansContainer'\n    variableDescription: 'The Storage Account container where Savings Plans Usage exports are dumped to'\n    ingestSchedule: 'AzureOptimization_IngestSavingsPlansUsageDaily'\n    ingestDescription: 'Daily Savings Plans Usage ingests'\n    ingestTimeOffset: 'PT2H35M'\n    ingestFrequency: 'Day'\n    ingestJobId: savingsPlansUsageIngestJobId\n    exportSchedule: savingsPlansUsageExportsScheduleName\n    exportJobId: savingsPlansUsageExportJobId\n  }\n]\nvar csvParameterizedExports = [\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorVmssCpuMaxExportsScheduleName\n    exportJobId: monitorVmssCpuMaxExportJobId\n    parameters: {\n      ResourceType: 'microsoft.compute/virtualmachinescalesets'\n      TimeSpan: '01:00:00'\n      aggregationType: 'Maximum'\n      MetricNames: 'Percentage CPU'\n      TimeGrain: '01:00:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorVmssCpuAvgExportsScheduleName\n    exportJobId: monitorVmssCpuAvgExportJobId\n    parameters: {\n      ResourceType: 'microsoft.compute/virtualmachinescalesets'\n      TimeSpan: '01:00:00'\n      aggregationType: 'Average'\n      MetricNames: 'Percentage CPU'\n      TimeGrain: '01:00:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorVmssMemoryMinExportsScheduleName\n    exportJobId: monitorVmssMemoryMinExportJobId\n    parameters: {\n      ResourceType: 'microsoft.compute/virtualmachinescalesets'\n      TimeSpan: '01:00:00'\n      aggregationType: 'Minimum'\n      MetricNames: 'Available Memory Bytes'\n      TimeGrain: '01:00:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorSqlDbDtuMaxExportsScheduleName\n    exportJobId: monitorSqlDbDtuMaxExportJobId\n    parameters: {\n      ResourceType: 'microsoft.sql/servers/databases'\n      ARGFilter: 'sku.tier in (\\'Standard\\',\\'Premium\\')'\n      TimeSpan: '01:00:00'\n      aggregationType: 'Maximum'\n      MetricNames: 'dtu_consumption_percent'\n      TimeGrain: '01:00:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorSqlDbDtuAvgExportsScheduleName\n    exportJobId: monitorSqlDbDtuAvgExportJobId\n    parameters: {\n      ResourceType: 'microsoft.sql/servers/databases'\n      ARGFilter: 'sku.tier in (\\'Standard\\',\\'Premium\\')'\n      TimeSpan: '01:00:00'\n      aggregationType: 'Average'\n      AggregationOfType: 'Maximum'\n      MetricNames: 'dtu_consumption_percent'\n      TimeGrain: '00:01:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorAppServiceCpuMaxExportsScheduleName\n    exportJobId: monitorAppServiceCpuMaxExportJobId\n    parameters: {\n      ResourceType: 'microsoft.web/serverfarms'\n      ARGFilter: 'properties.computeMode == \\'Dedicated\\' and sku.tier != \\'Free\\''\n      TimeSpan: '01:00:00'\n      aggregationType: 'Maximum'\n      MetricNames: 'CpuPercentage'\n      TimeGrain: '01:00:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorAppServiceCpuAvgExportsScheduleName\n    exportJobId: monitorAppServiceCpuAvgExportJobId\n    parameters: {\n      ResourceType: 'microsoft.web/serverfarms'\n      ARGFilter: 'properties.computeMode == \\'Dedicated\\' and sku.tier != \\'Free\\''\n      TimeSpan: '01:00:00'\n      aggregationType: 'Average'\n      AggregationOfType: 'Maximum'\n      MetricNames: 'CpuPercentage'\n      TimeGrain: '00:01:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorAppServiceMemoryMaxExportsScheduleName\n    exportJobId: monitorAppServiceMemoryMaxExportJobId\n    parameters: {\n      ResourceType: 'microsoft.web/serverfarms'\n      ARGFilter: 'properties.computeMode == \\'Dedicated\\' and sku.tier != \\'Free\\''\n      TimeSpan: '01:00:00'\n      aggregationType: 'Maximum'\n      MetricNames: 'MemoryPercentage'\n      TimeGrain: '01:00:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorAppServiceMemoryAvgExportsScheduleName\n    exportJobId: monitorAppServiceMemoryAvgExportJobId\n    parameters: {\n      ResourceType: 'microsoft.web/serverfarms'\n      ARGFilter: 'properties.computeMode == \\'Dedicated\\' and sku.tier != \\'Free\\''\n      TimeSpan: '01:00:00'\n      aggregationType: 'Average'\n      AggregationOfType: 'Maximum'\n      MetricNames: 'MemoryPercentage'\n      TimeGrain: '00:01:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorDiskIOPSAvgExportsScheduleName\n    exportJobId: monitorDiskIOPSAvgExportJobId\n    parameters: {\n      ResourceType: 'microsoft.compute/disks'\n      ARGFilter: 'sku.name =~ \\'Premium_LRS\\' and properties.diskState != \\'Unattached\\''\n      TimeSpan: '01:00:00'\n      aggregationType: 'Average'\n      AggregationOfType: 'Maximum'\n      MetricNames: 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec'\n      TimeGrain: '00:01:00'\n    }\n  }\n  {\n    runbookName: monitorExportsRunbookName\n    exportSchedule: monitorDiskMBPsAvgExportsScheduleName\n    exportJobId: monitorDiskMBPsAvgExportJobId\n    parameters: {\n      ResourceType: 'microsoft.compute/disks'\n      ARGFilter: 'sku.name =~ \\'Premium_LRS\\' and properties.diskState != \\'Unattached\\''\n      TimeSpan: '01:00:00'\n      aggregationType: 'Average'\n      AggregationOfType: 'Maximum'\n      MetricNames: 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec'\n      TimeGrain: '00:01:00'\n    }\n  }\n]\nvar unattachedDisksRecommendationsRunbookName = 'Recommend-UnattachedDisksToBlobStorage'\nvar advisorCostAugmentedRecommendationsRunbookName = 'Recommend-AdvisorCostAugmentedToBlobStorage'\nvar advisorAsIsRecommendationsRunbookName = 'Recommend-AdvisorAsIsToBlobStorage'\nvar vmsHARecommendationsRunbookName = 'Recommend-VMsHighAvailabilityToBlobStorage'\nvar vmOptimizationsRecommendationsRunbookName = 'Recommend-VMOptimizationsToBlobStorage'\nvar aadExpiringCredsRecommendationsRunbookName = 'Recommend-AADExpiringCredentialsToBlobStorage'\nvar unusedLBsRecommendationsRunbookName = 'Recommend-UnusedLoadBalancersToBlobStorage'\nvar unusedAppGWsRecommendationsRunbookName = 'Recommend-UnusedAppGWsToBlobStorage'\nvar armOptimizationsRecommendationsRunbookName = 'Recommend-ARMOptimizationsToBlobStorage'\nvar vnetOptimizationsRecommendationsRunbookName = 'Recommend-VNetOptimizationsToBlobStorage'\nvar vmssOptimizationsRecommendationsRunbookName = 'Recommend-VMSSOptimizationsToBlobStorage'\nvar sqldbOptimizationsRecommendationsRunbookName = 'Recommend-SqlDbOptimizationsToBlobStorage'\nvar storageOptimizationsRecommendationsRunbookName = 'Recommend-StorageAccountOptimizationsToBlobStorage'\nvar appServiceOptimizationsRecommendationsRunbookName = 'Recommend-AppServiceOptimizationsToBlobStorage'\nvar diskOptimizationsRecommendationsRunbookName = 'Recommend-DiskOptimizationsToBlobStorage'\nvar cleanUpOlderRecommendationsRunbookName = 'CleanUp-OlderRecommendationsFromSqlServer'\nvar recommendations = [\n  {\n    recommendationJobId: unattachedDisksRecommendationJobId\n    runbookName: unattachedDisksRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: advisorCostAugmentedRecommendationJobId\n    runbookName: advisorCostAugmentedRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: advisorAsIsRecommendationJobId\n    runbookName: advisorAsIsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: vmsHaRecommendationJobId\n    runbookName: vmsHARecommendationsRunbookName\n  }\n  {\n    recommendationJobId: vmOptimizationsRecommendationJobId\n    runbookName: vmOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: aadExpiringCredsRecommendationJobId\n    runbookName: aadExpiringCredsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: unusedLoadBalancersRecommendationJobId\n    runbookName: unusedLBsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: unusedAppGWsRecommendationJobId\n    runbookName: unusedAppGWsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: armOptimizationsRecommendationJobId\n    runbookName: armOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: vnetOptimizationsRecommendationJobId\n    runbookName: vnetOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: vmssOptimizationsRecommendationJobId\n    runbookName: vmssOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: sqldbOptimizationsRecommendationJobId\n    runbookName: sqldbOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: storageOptimizationsRecommendationJobId\n    runbookName: storageOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: appServiceOptimizationsRecommendationJobId\n    runbookName: appServiceOptimizationsRecommendationsRunbookName\n  }\n  {\n    recommendationJobId: diskOptimizationsRecommendationJobId\n    runbookName: diskOptimizationsRecommendationsRunbookName\n  }\n]\nvar remediationLogsContainerName = 'remediationlogs'\nvar recommendationsContainerName = 'recommendationsexports'\nvar csvIngestRunbookName = 'Ingest-OptimizationCSVExportsToLogAnalytics'\nvar recommendationsIngestRunbookName = 'Ingest-RecommendationsToSQLServer'\nvar recommendationsLogAnalyticsIngestRunbookName = 'Ingest-RecommendationsToLogAnalytics'\nvar suppressionsLogAnalyticsIngestRunbookName = 'Ingest-SuppressionsToLogAnalytics'\nvar advisorRightSizeFilteredRemediationRunbookName = 'Remediate-AdvisorRightSizeFiltered'\nvar longDeallocatedVMsFilteredRemediationRunbookName = 'Remediate-LongDeallocatedVMsFiltered'\nvar unattachedDisksFilteredRemediationRunbookName = 'Remediate-UnattachedDisksFiltered'\nvar remediationLogsIngestScheduleName = 'AzureOptimization_IngestRemediationLogsDaily'\nvar recommendationsScheduleName = 'AzureOptimization_RecommendationsWeekly'\nvar recommendationsIngestScheduleName = 'AzureOptimization_IngestRecommendationsWeekly'\nvar suppressionsIngestScheduleName = 'AzureOptimization_IngestSuppressionsWeekly'\nvar recommendationsCleanUpScheduleName = 'AzureOptimization_CleanUpRecommendationsWeekly'\nvar Az_Accounts = {\n  name: 'Az.Accounts'\n  url: 'https://www.powershellgallery.com/api/v2/package/Az.Accounts/2.12.1'\n}\nvar Microsoft_Graph_Authentication = {\n  name: 'Microsoft.Graph.Authentication'\n  url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Authentication/2.4.0'\n}\nvar psModules = [\n  {\n    name: 'Az.Compute'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.Compute/5.7.0'\n  }\n  {\n    name: 'Az.OperationalInsights'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.OperationalInsights/3.2.0'\n  }\n  {\n    name: 'Az.ResourceGraph'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.ResourceGraph/0.13.0'\n  }\n  {\n    name: 'Az.Storage'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.Storage/5.5.0'\n  }\n  {\n    name: 'Az.Resources'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.Resources/6.6.0'\n  }\n  {\n    name: 'Az.Monitor'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.Monitor/4.4.1'\n  }\n  {\n    name: 'Az.PolicyInsights'\n    url: 'https://www.powershellgallery.com/api/v2/package/Az.PolicyInsights/1.6.0'\n  }\n  {\n    name: 'Microsoft.Graph.Users'\n    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Users/2.4.0'\n  }\n  {\n    name: 'Microsoft.Graph.Groups'\n    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Groups/2.4.0'\n  }\n  {\n    name: 'Microsoft.Graph.Applications'\n    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Applications/2.4.0'\n  }\n  {\n    name: 'Microsoft.Graph.Identity.DirectoryManagement'\n    url: 'https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Identity.DirectoryManagement/2.4.0'\n  }\n]\nvar runbooks = [\n  {\n    name: advisorExportsRunbookName\n    version: '1.4.2.1'\n    description: 'Exports Azure Advisor recommendations to Blob Storage using the Advisor API'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${advisorExportsRunbookName}.ps1')\n  }\n  {\n    name: argDisksExportsRunbookName\n    version: '1.3.4.1'\n    description: 'Exports Managed Disks properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argDisksExportsRunbookName}.ps1')\n  }\n  {\n    name: argVhdExportsRunbookName\n    version: '1.1.4.1'\n    description: 'Exports Unmanaged Disks (owned by a VM) properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVhdExportsRunbookName}.ps1')\n  }\n  {\n    name: argVmExportsRunbookName\n    version: '1.4.4.1'\n    description: 'Exports Virtual Machine properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmExportsRunbookName}.ps1')\n  }\n  {\n    name: argVmssExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports VMSS properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVmssExportsRunbookName}.ps1')\n  }\n  {\n    name: argAvailSetExportsRunbookName\n    version: '1.1.4.1'\n    description: 'Exports Availability Set properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAvailSetExportsRunbookName}.ps1')\n  }\n  {\n    name: consumptionExportsRunbookName\n    version: '2.0.4.1'\n    description: 'Exports Azure Consumption events to Blob Storage using Azure Consumption API'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${consumptionExportsRunbookName}.ps1')\n  }\n  {\n    name: aadObjectsExportsRunbookName\n    version: '1.2.2.1'\n    description: 'Exports Azure AAD Objects to Blob Storage using Azure ARM API'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${aadObjectsExportsRunbookName}.ps1')\n  }\n  {\n    name: argLoadBalancersExportsRunbookName\n    version: '1.1.4.1'\n    description: 'Exports Load Balancer properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argLoadBalancersExportsRunbookName}.ps1')\n  }\n  {\n    name: argAppGWsExportsRunbookName\n    version: '1.1.4.1'\n    description: 'Exports Application Gateway properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppGWsExportsRunbookName}.ps1')\n  }\n  {\n    name: argResContainersExportsRunbookName\n    version: '1.0.5.1'\n    description: 'Exports Resource Containers properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argResContainersExportsRunbookName}.ps1')\n  }\n  {\n    name: rbacExportsRunbookName\n    version: '1.0.4.1'\n    description: 'Exports RBAC assignments to Blob Storage using ARM and Microsoft Entra'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${rbacExportsRunbookName}.ps1')\n  }\n  {\n    name: argNICExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports NIC properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNICExportsRunbookName}.ps1')\n  }\n  {\n    name: argNSGExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports NSG properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argNSGExportsRunbookName}.ps1')\n  }\n  {\n    name: argPublicIpExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports Public IP properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argPublicIpExportsRunbookName}.ps1')\n  }\n  {\n    name: argVNetExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports VNet properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argVNetExportsRunbookName}.ps1')\n  }\n  {\n    name: argSqlDbExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports SQL DB properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argSqlDbExportsRunbookName}.ps1')\n  }\n  {\n    name: policyStateExportsRunbookName\n    version: '1.0.3.1'\n    description: 'Exports Azure Policy State to Blob Storage'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${policyStateExportsRunbookName}.ps1')\n  }\n  {\n    name: monitorExportsRunbookName\n    version: '1.0.2.1'\n    description: 'Exports Azure Monitor metrics to Blob Storage'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${monitorExportsRunbookName}.ps1')\n  }\n  {\n    name: argAppServicePlanExportsRunbookName\n    version: '1.0.1.1'\n    description: 'Exports App Service Plan properties to Blob Storage using Azure Resource Graph'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${argAppServicePlanExportsRunbookName}.ps1')\n  }\n  {\n    name: reservationsExportsRunbookName\n    version: '1.1.2.1'\n    description: 'Exports Reservations Usage to Blob Storage using the EA or MCA APIs'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsExportsRunbookName}.ps1')\n  }\n  {\n    name: reservationsPriceExportsRunbookName\n    version: '1.0.1.1'\n    description: 'Exports Reservations Prices to Blob Storage using the Retail Prices API'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${reservationsPriceExportsRunbookName}.ps1')\n  }\n  {\n    name: priceSheetExportsRunbookName\n    version: '1.1.1.1'\n    description: 'Exports Price Sheet to Blob Storage using the EA or MCA APIs'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${priceSheetExportsRunbookName}.ps1')\n  }\n  {\n    name: savingsPlansExportsRunbookName\n    version: '1.0.0.0'\n    description: 'Exports Savings Plans Usage to Blob Storage using the EA or MCA APIs'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${savingsPlansExportsRunbookName}.ps1')\n  }\n  {\n    name: csvIngestRunbookName\n    version: '1.5.0.0'\n    description: 'Ingests CSV blobs as custom logs to Log Analytics'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/data-collection/${csvIngestRunbookName}.ps1')\n  }\n  {\n    name: unattachedDisksRecommendationsRunbookName\n    version: '2.4.8.0'\n    description: 'Generates unattached disks recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${unattachedDisksRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: advisorCostAugmentedRecommendationsRunbookName\n    version: '2.9.1.0'\n    description: 'Generates augmented Advisor Cost recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorCostAugmentedRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: advisorAsIsRecommendationsRunbookName\n    version: '1.5.5.0'\n    description: 'Generates all types of Advisor recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${advisorAsIsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: vmsHARecommendationsRunbookName\n    version: '1.0.3.0'\n    description: 'Generates VMs High Availability recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmsHARecommendationsRunbookName}.ps1')\n  }\n  {\n    name: vmOptimizationsRecommendationsRunbookName\n    version: '1.0.0.0'\n    description: 'Generates VM optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: aadExpiringCredsRecommendationsRunbookName\n    version: '1.1.10.0'\n    description: 'Generates AAD Objects with expiring credentials recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${aadExpiringCredsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: unusedLBsRecommendationsRunbookName\n    version: '1.2.9.0'\n    description: 'Generates unused Load Balancers recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedLBsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: unusedAppGWsRecommendationsRunbookName\n    version: '1.2.9.0'\n    description: 'Generates unused Application Gateways recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${unusedAppGWsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: armOptimizationsRecommendationsRunbookName\n    version: '1.0.3.0'\n    description: 'Generates ARM optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${armOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: vnetOptimizationsRecommendationsRunbookName\n    version: '1.0.4.0'\n    description: 'Generates Virtual Network optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vnetOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: vmssOptimizationsRecommendationsRunbookName\n    version: '1.1.1.0'\n    description: 'Generates VM Scale Set optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${vmssOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: sqldbOptimizationsRecommendationsRunbookName\n    version: '1.1.2.0'\n    description: 'Generates SQL DB optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${sqldbOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: storageOptimizationsRecommendationsRunbookName\n    version: '1.0.3.0'\n    description: 'Generates Storage Account optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${storageOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: appServiceOptimizationsRecommendationsRunbookName\n    version: '1.0.3.0'\n    description: 'Generates App Service optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${appServiceOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: diskOptimizationsRecommendationsRunbookName\n    version: '1.1.1.0'\n    description: 'Generates Disk optimizations recommendations'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${diskOptimizationsRecommendationsRunbookName}.ps1')\n  }\n  {\n    name: recommendationsIngestRunbookName\n    version: '1.6.5.0'\n    description: 'Ingests JSON-based recommendations into an Azure SQL Database'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsIngestRunbookName}.ps1')\n  }\n  {\n    name: recommendationsLogAnalyticsIngestRunbookName\n    version: '1.0.2.0'\n    description: 'Ingests JSON-based recommendations into Log Analytics'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${recommendationsLogAnalyticsIngestRunbookName}.ps1')\n  }\n  {\n    name: suppressionsLogAnalyticsIngestRunbookName\n    version: '1.0.0.0'\n    description: 'Ingests suppressions into Log Analytics'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/recommendations/${suppressionsLogAnalyticsIngestRunbookName}.ps1')\n  }\n  {\n    name: advisorRightSizeFilteredRemediationRunbookName\n    version: '1.2.4.0'\n    description: 'Remediates Azure Advisor right-size recommendations given fit and tag filters'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/remediations/${advisorRightSizeFilteredRemediationRunbookName}.ps1')\n  }\n  {\n    name: longDeallocatedVMsFilteredRemediationRunbookName\n    version: '1.0.3.0'\n    description: 'Remediates long-deallocated VMs recommendations given fit and tag filters'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/remediations/${longDeallocatedVMsFilteredRemediationRunbookName}.ps1')\n  }\n  {\n    name: unattachedDisksFilteredRemediationRunbookName\n    version: '1.0.3.0'\n    description: 'Remediates unattached disks recommendations given fit and tag filters'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/remediations/${unattachedDisksFilteredRemediationRunbookName}.ps1')\n  }\n  {\n    name: cleanUpOlderRecommendationsRunbookName\n    version: '1.0.0.0'\n    description: 'Cleans up older recommendations from SQL Database'\n    type: 'PowerShell'\n    scriptUri: uri(templateLocation, 'runbooks/maintenance/${cleanUpOlderRecommendationsRunbookName}.ps1')\n  }\n]\nvar automationVariables = [\n  {\n    name: 'AzureOptimization_CloudEnvironment'\n    description: 'Azure Cloud environment (e.g., AzureCloud, AzureChinaCloud, etc.)'\n    value: '\"${cloudEnvironment}\"'\n  }\n  {\n    name: 'AzureOptimization_AuthenticationOption'\n    description: 'Runbook authentication type (RunAsAccount or ManagedIdentity)'\n    value: '\"${authenticationOption}\"'\n  }\n  {\n    name: 'AzureOptimization_StorageSink'\n    description: 'The Azure Storage Account where data source exports are dumped to'\n    value: '\"${storageAccountName}\"'\n  }\n  {\n    name: 'AzureOptimization_StorageSinkRG'\n    description: 'The resource group for the Azure Storage Account sink'\n    value: '\"${resourceGroup().name}\"'\n  }\n  {\n    name: 'AzureOptimization_StorageSinkSubId'\n    description: 'The subscription Id for the Azure Storage Account sink'\n    value: '\"${subscription().subscriptionId}\"'\n  }\n  {\n    name: 'AzureOptimization_ConsumptionOffsetDays'\n    description: 'The offset (in days) for querying for consumption data'\n    value: 3\n  }\n  {\n    name: 'AzureOptimization_AdvisorFilter'\n    description: 'The category filter to use for Azure Advisor (non-Cost) recommendations exports'\n    value: '\"HighAvailability,Security,Performance,OperationalExcellence\"'\n  }\n  {\n    name: 'AzureOptimization_ReferenceRegion'\n    description: 'The Azure region used as a reference for getting details about Azure VM sizes available'\n    value: '\"${projectLocation}\"'\n  }\n  {\n    name: 'AzureOptimization_SQLServerDatabase'\n    description: 'The Azure SQL Database name for the ingestion control and recommendations tables'\n    value: '\"${sqlDatabaseName}\"'\n  }\n  {\n    name: 'AzureOptimization_LogAnalyticsChunkSize'\n    description: 'The size (in rows) for each chunk of Log Analytics ingestion request'\n    value: 6000\n  }\n  {\n    name: 'AzureOptimization_StorageBlobsPageSize'\n    description: 'The size (in blobs count) for each page of Storage Account container blob listing'\n    value: 1000\n  }\n  {\n    name: 'AzureOptimization_SQLServerInsertSize'\n    description: 'The size (in inserted lines) for each page of recommendations ingestion into the SQL Database'\n    value: 900\n  }\n  {\n    name: 'AzureOptimization_LogAnalyticsLogPrefix'\n    description: 'The prefix for all Azure Optimization custom log tables in Log Analytics'\n    value: '\"AzureOptimization\"'\n  }\n  {\n    name: 'AzureOptimization_LogAnalyticsWorkspaceName'\n    description: 'The Log Analytics Workspace Name where optimization data will be ingested'\n    value: '\"${logAnalyticsWorkspaceName}\"'\n  }\n  {\n    name: 'AzureOptimization_LogAnalyticsWorkspaceRG'\n    description: 'The resource group for the Log Analytics Workspace where optimization data will be ingested'\n    value: '\"${((!logAnalyticsReuse) ? resourceGroup().name : logAnalyticsWorkspaceRG)}\"'\n  }\n  {\n    name: 'AzureOptimization_LogAnalyticsWorkspaceSubId'\n    description: 'The Azure subscription for the Log Analytics Workspace where optimization data will be ingested'\n    value: '\"${subscription().subscriptionId}\"'\n  }\n  {\n    name: 'AzureOptimization_LogAnalyticsWorkspaceTenantId'\n    description: 'The Microsoft Entra tenant for the Log Analytics Workspace where optimization data will be ingested'\n    value: '\"${subscription().tenantId}\"'\n  }\n  {\n    name: 'AzureOptimization_PriceSheetMeterCategories'\n    description: 'Comma-separated meter categories to be included in the Price Sheet (remove variable to include all categories)'\n    value: '\"Virtual Machines,Storage\"'\n  }\n  {\n    name: 'AzureOptimization_RetailPricesCurrencyCode'\n    description: 'The currency code to be used for the retail prices exports (used for Reservations prices)'\n    value: '\"EUR\"'\n  }\n  {\n    name: 'AzureOptimization_RecommendAdvisorPeriodInDays'\n    description: 'The period (in days) to look back for Advisor exported recommendations'\n    value: 7\n  }\n  {\n    name: 'AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays'\n    description: 'The period (in days) for considering a VM long deallocated'\n    value: 30\n  }\n  {\n    name: 'AzureOptimization_PerfPercentileCpu'\n    description: 'The percentile to be used for processor metrics'\n    value: 99\n  }\n  {\n    name: 'AzureOptimization_PerfPercentileMemory'\n    description: 'The percentile to be used for memory metrics'\n    value: 99\n  }\n  {\n    name: 'AzureOptimization_PerfPercentileNetwork'\n    description: 'The percentile to be used for network metrics'\n    value: 99\n  }\n  {\n    name: 'AzureOptimization_PerfPercentileDisk'\n    description: 'The percentile to be used for disk metrics'\n    value: 99\n  }\n  {\n    name: 'AzureOptimization_PerfPercentileSqlDtu'\n    description: 'The percentile to be used for SQL DB DTU metrics'\n    value: 99\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdCpuPercentage'\n    description: 'The processor usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized'\n    value: 30\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdMemoryPercentage'\n    description: 'The memory usage percentage threshold above which the fit score is decreased or below which the instance is considered underutilized'\n    value: 50\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdCpuDegradedMaxPercentage'\n    description: 'The maximum processor usage percentage threshold above which the instance is considered degraded'\n    value: 95\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdCpuDegradedAvgPercentage'\n    description: 'The average processor usage percentage threshold above which the instance is considered degraded'\n    value: 75\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdMemoryDegradedPercentage'\n    description: 'The memory usage percentage threshold above which the instance is considered degraded'\n    value: 90\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdNetworkMbps'\n    description: 'The network usage threshold (in Mbps) above which the fit score is decreased'\n    value: 750\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdCpuShutdownPercentage'\n    description: 'The processor usage percentage threshold above which the fit score is decreased (shutdown scenarios)'\n    value: 5\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdMemoryShutdownPercentage'\n    description: 'The memory usage percentage threshold above which the fit score is decreased (shutdown scenarios)'\n    value: 100\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdNetworkShutdownMbps'\n    description: 'The network usage threshold (in Mbps) above which the fit score is decreased (shutdown scenarios)'\n    value: 10\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdDtuPercentage'\n    description: 'The DTU usage percentage threshold below which a SQL Database instance is considered underutilized'\n    value: 40\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdDtuDegradedPercentage'\n    description: 'The DTU usage percentage threshold above which a SQL Database instance is considered performance degraded'\n    value: 75\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdDiskIOPSPercentage'\n    description: 'The IOPS usage percentage threshold below which a Disk is considered underutilized'\n    value: 5\n  }\n  {\n    name: 'AzureOptimization_PerfThresholdDiskMBsPercentage'\n    description: 'The throughput (MBps) usage percentage threshold below which a Disk is considered underutilized'\n    value: 5\n  }\n  {\n    name: 'AzureOptimization_RemediateRightSizeMinFitScore'\n    description: 'The minimum fit score for right-size remediation'\n    value: '\"5.0\"'\n  }\n  {\n    name: 'AzureOptimization_RemediateRightSizeMinWeeksInARow'\n    description: 'The minimum number of weeks in a row required for a right-size recommendation to be remediated'\n    value: 4\n  }\n  {\n    name: 'AzureOptimization_RecommendationAdvisorCostRightSizeId'\n    description: 'The Azure Advisor VM right-size recommendation ID'\n    value: '\"e10b1381-5f0a-47ff-8c7b-37bd13d7c974\"'\n  }\n  {\n    name: 'AzureOptimization_RemediateLongDeallocatedVMsMinFitScore'\n    description: 'The minimum fit score for long-deallocated VM remediation'\n    value: '\"5.0\"'\n  }\n  {\n    name: 'AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow'\n    description: 'The minimum number of weeks in a row required for a long-deallocated VM recommendation to be remediated'\n    value: 4\n  }\n  {\n    name: 'AzureOptimization_RecommendationLongDeallocatedVMsId'\n    description: 'The long deallocated VM recommendation ID'\n    value: '\"c320b790-2e58-452a-aa63-7b62c383ad8a\"'\n  }\n  {\n    name: 'AzureOptimization_RemediateUnattachedDisksMinFitScore'\n    description: 'The minimum fit score for unattached disk remediation'\n    value: '\"5.0\"'\n  }\n  {\n    name: 'AzureOptimization_RemediateUnattachedDisksMinWeeksInARow'\n    description: 'The minimum number of weeks in a row required for a unattached disk recommendation to be remediated'\n    value: 4\n  }\n  {\n    name: 'AzureOptimization_RemediateUnattachedDisksAction'\n    description: 'The action for the unattached disk recommendation to be remediated (Delete or Downsize)'\n    value: '\"Delete\"'\n  }\n  {\n    name: 'AzureOptimization_RecommendationUnattachedDisksId'\n    description: 'The unattached disk recommendation ID'\n    value: '\"c84d5e86-e2d6-4d62-be7c-cecfbd73b0db\"'\n  }\n  {\n    name: 'AzureOptimization_RecommendationAADMinCredValidityDays'\n    description: 'The minimum validity of an AAD Object credential in days'\n    value: 30\n  }\n  {\n    name: 'AzureOptimization_RecommendationAADMaxCredValidityYears'\n    description: 'The maximum validity of an AAD Object credential in years'\n    value: 2\n  }\n  {\n    name: 'AzureOptimization_AADObjectsFilter'\n    description: 'The Microsoft Entra object types to export'\n    value: '\"Application,ServicePrincipal,User,Group\"'\n  }\n  {\n    name: 'AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold'\n    description: 'The percentage threshold (used to trigger recommendations) for total RBAC assignments limits'\n    value: 80\n  }\n  {\n    name: 'AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold'\n    description: 'The percentage threshold (used to trigger recommendations) for resource group count limits'\n    value: 80\n  }\n  {\n    name: 'AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold'\n    description: 'The percentage threshold (used to trigger recommendations) for maximum subnet address space usage'\n    value: 80\n  }\n  {\n    name: 'AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold'\n    description: 'The percentage threshold (used to trigger recommendations) for minimum subnet address space usage'\n    value: 5\n  }\n  {\n    name: 'AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays'\n    description: 'The minimum age (in days) for an empty subnet to trigger an NSG rule recommendation'\n    value: 30\n  }\n  {\n    name: 'AzureOptimization_RecommendationsMaxAgeInDays'\n    description: 'The maximum age (in days) for a recommendation to be kept in the SQL database'\n    value: 365\n  }\n  {\n    name: 'AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage'\n    description: 'The minimum Storage Account growth percentage required to flag Storage as not having a retention policy in place'\n    value: 5\n  }\n  {\n    name: 'AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold'\n    description: 'The minimum monthly cost (in your EA/MCA currency) required to flag Storage as not having a retention policy in place'\n    value: 50\n  }\n  {\n    name: 'AzureOptimization_RecommendationStorageAcountGrowthLookbackDays'\n    description: 'The lookback period (in days) for analyzing Storage Account growth'\n    value: 30\n  }\n]\n\nresource logAnalyticsWorkspace 'microsoft.operationalinsights/workspaces@2020-08-01' = if (!logAnalyticsReuse) {\n  name: logAnalyticsWorkspaceName\n  location: projectLocation\n  tags: resourceTags\n  properties: {\n    sku: {\n      name: 'pergb2018'\n    }\n    retentionInDays: logAnalyticsRetentionDays\n  }\n}\n\nresource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {\n  name: storageAccountName\n  location: projectLocation\n  tags: resourceTags\n  sku: {\n    name: 'Standard_LRS'\n  }\n  kind: 'StorageV2'\n  properties: {\n    allowBlobPublicAccess: false\n    networkAcls: {\n      bypass: 'AzureServices'\n      virtualNetworkRules: []\n      ipRules: []\n      defaultAction: 'Allow'\n    }\n    supportsHttpsTrafficOnly: true\n    encryption: {\n      services: {\n        file: {\n          enabled: true\n        }\n        blob: {\n          enabled: true\n        }\n      }\n      keySource: 'Microsoft.Storage'\n    }\n    minimumTlsVersion: 'TLS1_2'\n    accessTier: 'Cool'\n  }\n}\n\nresource storageBlobServices 'Microsoft.Storage/storageAccounts/blobServices@2022-09-01' = {\n  parent: storageAccount\n  name: 'default'\n  properties: {\n    cors: {\n      corsRules: []\n    }\n    deleteRetentionPolicy: {\n      enabled: false\n    }\n  }\n}\n\nresource storageCsvExportsContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = [for item in csvExports: {\n  name: '${storageAccountName}/default/${item.containerName}'\n  properties: {\n    publicAccess: 'None'\n  }\n  dependsOn: [\n    storageBlobServices\n    storageAccount\n  ]\n}]\n\nresource storageRecommendationsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = {\n  name: '${storageAccountName}/default/${recommendationsContainerName}'\n  properties: {\n    publicAccess: 'None'\n  }\n  dependsOn: [\n    storageBlobServices\n    storageAccount\n  ]\n}\n\nresource storageRemediationLogsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2022-09-01' = {\n  name: '${storageAccountName}/default/${remediationLogsContainerName}'\n  properties: {\n    publicAccess: 'None'\n  }\n  dependsOn: [\n    storageBlobServices\n    storageAccount\n  ]\n}\n\nresource storageLifecycleManagementPolicy 'Microsoft.Storage/storageAccounts/managementPolicies@2021-02-01' = {\n  parent: storageAccount\n  name: 'default'\n  properties: {\n    policy: {\n      rules: [\n        {\n          enabled: true\n          name: 'Clean6MonthsOldBlobs'\n          type: 'Lifecycle'\n          definition: {\n            actions: {\n              baseBlob: {\n                delete: {\n                  daysAfterModificationGreaterThan: 180\n                }\n              }\n              snapshot: {\n                delete: {\n                  daysAfterCreationGreaterThan: 180\n                }\n              }\n              version: {\n                delete: {\n                  daysAfterCreationGreaterThan: 180\n                }\n              }\n            }\n            filters: {\n              blobTypes: [\n                'blockBlob'\n              ]\n            }\n          }\n        }\n      ]\n    }\n  }\n  dependsOn: [\n    storageBlobServices\n  ]\n}\n\nresource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {\n  name: sqlServerName\n  location: projectLocation\n  tags: resourceTags\n  properties: {\n    administratorLogin: sqlAdminLogin\n    administratorLoginPassword: sqlAdminPassword\n    version: '12.0'\n    publicNetworkAccess: 'Enabled'\n    minimalTlsVersion: '1.2'\n  }\n}\n\nresource sqlServerFirewall 'Microsoft.Sql/servers/firewallRules@2022-05-01-preview' = {\n  parent: sqlServer\n  name: 'AllowAllWindowsAzureIps'\n  properties: {\n    endIpAddress: '0.0.0.0'\n    startIpAddress: '0.0.0.0'\n  }\n}\n\nresource sqlDatabase 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {\n  parent: sqlServer\n  name: sqlDatabaseName\n  location: projectLocation\n  tags: resourceTags\n  sku: {\n    name: 'Basic'\n    tier: 'Basic'\n    capacity: 5\n  }\n  properties: {\n    collation: 'SQL_Latin1_General_CP1_CI_AS'\n    maxSizeBytes: 2147483648\n    catalogCollation: 'SQL_Latin1_General_CP1_CI_AS'\n    zoneRedundant: false\n    readScale: 'Disabled'\n    autoPauseDelay: 60\n    requestedBackupStorageRedundancy: 'Geo'\n  }\n}\n\nresource sqlServerName_sqlDatabaseName_default 'Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies@2022-05-01-preview' = {\n  name: '${sqlServerName}/${sqlDatabaseName}/default'\n  properties: {\n    retentionDays: sqlBackupRetentionDays\n  }\n  dependsOn: [\n    sqlDatabase\n    sqlServer\n  ]\n}\n\nresource automationAccount 'Microsoft.Automation/automationAccounts@2020-01-13-preview' = {\n  name: automationAccountName\n  location: projectLocation\n  tags: resourceTags\n  identity: {\n    type: 'SystemAssigned'\n  }\n  properties: {\n    sku: {\n      name: 'Basic'\n    }\n  }\n}\n\nresource automationModule_Az_Accounts 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: Az_Accounts.name\n  tags: resourceTags\n  properties: {\n    contentLink: {\n      uri: Az_Accounts.url\n    }\n  }\n}\n\nresource automationModule_Microsoft_Graph_Authentication 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: Microsoft_Graph_Authentication.name\n  tags: resourceTags\n  properties: {\n    contentLink: {\n      uri: Microsoft_Graph_Authentication.url\n    }\n  }\n}\n\nresource automationModule_All 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = [for item in psModules: {\n  parent: automationAccount  \n  name: item.name\n  tags: resourceTags\n  properties: {\n    contentLink: {\n      uri: item.url\n    }\n  }\n  dependsOn: [\n    automationModule_Az_Accounts\n    automationModule_Microsoft_Graph_Authentication\n  ]\n}]\n\nresource automationRunbooks 'Microsoft.Automation/automationAccounts/runbooks@2020-01-13-preview' = [for item in runbooks: {\n  parent: automationAccount  \n  name: item.name\n  tags: resourceTags\n  location: projectLocation\n  properties: {\n    runbookType: item.type\n    logProgress: false\n    logVerbose: false\n    description: item.description\n    publishContentLink: {\n      uri: item.scriptUri\n      version: item.version\n    }\n  }\n  dependsOn: [\n    automationModule_All\n  ]\n}]\n\nresource automationVariablesAll 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in automationVariables: {\n  parent: automationAccount  \n  name: item.name\n  properties: {\n    description: item.description\n    value: item.value\n  }\n}]\n\nresource automationVariables_csvExports 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = [for item in csvExports: {\n  parent: automationAccount  \n  name: item.variableName\n  properties: {\n    description: item.variableDescription\n    value: '\"${item.containerName}\"'\n  }\n}]\n\nresource automationVariables_SQLServerHostname 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = {\n  parent: automationAccount  \n  name: 'AzureOptimization_SQLServerHostname'\n  properties: {\n    description: 'The Azure SQL Server hostname for the ingestion control and recommendations tables'\n    value: '\"${sqlServer.properties.fullyQualifiedDomainName}\"'\n  }\n}\n\nresource automationVariables_LogAnalyticsWorkspaceId 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = {\n  parent: automationAccount  \n  name: 'AzureOptimization_LogAnalyticsWorkspaceId'\n  properties: {\n    description: 'The Log Analytics Workspace ID where optimization data will be ingested'\n    value: '\"${reference(((!logAnalyticsReuse) ? logAnalyticsWorkspace.id : resourceId(logAnalyticsWorkspaceRG, 'microsoft.operationalinsights/workspaces', logAnalyticsWorkspaceName)), '2020-08-01').customerId}\"'\n  }\n}\n\nresource automationVariables_LogAnalyticsWorkspaceKey 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = {\n  parent: automationAccount  \n  name: 'AzureOptimization_LogAnalyticsWorkspaceKey'\n  properties: {\n    description: 'The shared key for the Log Analytics Workspace where optimization data will be ingested'\n    value: '\"${listKeys(((!logAnalyticsReuse) ? logAnalyticsWorkspace.id : resourceId(logAnalyticsWorkspaceRG, 'microsoft.operationalinsights/workspaces', logAnalyticsWorkspaceName)), '2020-08-01').primarySharedKey}\"'\n    isEncrypted: true\n  }\n}\n\nresource automatinCredentials_SQLServer 'Microsoft.Automation/automationAccounts/credentials@2020-01-13-preview' = {\n  parent: automationAccount  \n  name: 'AzureOptimization_SQLServerCredential'\n  properties: {\n    description: 'Azure Optimization SQL Database Credentials'\n    password: sqlAdminPassword\n    userName: sqlAdminLogin\n  }\n}\n\nresource automationSchedules_csvExports 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = [for item in csvExportsSchedules: {\n  parent: automationAccount\n  name: item.exportSchedule\n  properties: {\n    description: item.exportDescription\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, item.exportTimeOffset)\n    interval: 1\n    frequency: item.exportFrequency\n  }\n}]\n\nresource automationSchedules_csvIngests 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = [for item in csvExports: {\n  parent: automationAccount\n  name: item.ingestSchedule\n  properties: {\n    description: item.ingestDescription\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, item.ingestTimeOffset)\n    interval: 1\n    frequency: item.ingestFrequency\n  }\n}]\n\nresource automationSchedules_remediationCsvIngest 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: remediationLogsIngestScheduleName\n  properties: {\n    description: 'Starts the daily Remediation Logs ingests'\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, 'PT1H30M')\n    interval: 1\n    frequency: 'Day'\n  }\n}\n\nresource automationSchedules_recommendationsExport 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: recommendationsScheduleName\n  properties: {\n    description: 'Starts the weekly Recommendations generation'\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, 'PT2H30M')\n    interval: 1\n    frequency: 'Week'\n  }\n}\n\nresource automationSchedules_recommendationsIngest 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: recommendationsIngestScheduleName\n  properties: {\n    description: 'Starts the weekly Recommendations ingests'\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, 'PT3H30M')\n    interval: 1\n    frequency: 'Week'\n  }\n}\n\nresource automationSchedules_suppressionsIngest 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: suppressionsIngestScheduleName\n  properties: {\n    description: 'Starts the weekly Suppressions ingests'\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, 'PT3H00M')\n    interval: 1\n    frequency: 'Week'\n  }\n}\n\nresource automationSchedules_recommendationsCleanUp 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: recommendationsCleanUpScheduleName\n  properties: {\n    description: 'Starts the weekly Recommendations cleanup'\n    expiryTime: '9999-12-31T17:59:00-06:00'\n    startTime: dateTimeAdd(baseTime, 'P6D')\n    interval: 1\n    frequency: 'Week'\n  }\n}\n\nresource automationJobSchedules_csvExports 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in csvExports: if (!item.isOneToMany) {\n  parent: automationAccount\n  name: item.exportJobId\n  properties: {\n    schedule: {\n      name: item.exportSchedule\n    }\n    runbook: {\n      name: item.runbookName\n    }\n  }\n  dependsOn: [\n    automationSchedules_csvExports\n    automationModule_All\n    automationRunbooks\n  ]\n}]\n\nresource automationJobSchedules_csvParameterizedExports 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in csvParameterizedExports: {\n  parent: automationAccount\n  name: item.exportJobId\n  properties: {\n    schedule: {\n      name: item.exportSchedule\n    }\n    runbook: {\n      name: item.runbookName\n    }\n    parameters: item.parameters\n  }\n  dependsOn: [\n    automationSchedules_csvExports\n    automationModule_All\n    automationRunbooks\n  ]\n}]\n\nresource automationJobSchedules_csvIngests 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in csvExports: {\n  parent: automationAccount\n  name: item.ingestJobId\n  properties: {\n    schedule: {\n      name: item.ingestSchedule\n    }\n    runbook: {\n      name: csvIngestRunbookName\n    }\n    parameters: {\n      StorageSinkContainer: item.containerName\n    }\n  }\n  dependsOn: [\n    automationSchedules_csvIngests\n    automationModule_All\n    automationRunbooks\n  ]\n}]\n\nresource automationJobSchedules_remediationLogsIngests 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: remediationLogsIngestJobId\n  properties: {\n    schedule: {\n      name: remediationLogsIngestScheduleName\n    }\n    runbook: {\n      name: csvIngestRunbookName\n    }\n    parameters: {\n      StorageSinkContainer: remediationLogsContainerName\n    }\n  }\n  dependsOn: [\n    automationSchedules_remediationCsvIngest\n    automationModule_All\n    automationRunbooks\n  ]\n}\n\nresource automationJobSchedules_recommendationsExports 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = [for item in recommendations: {\n  parent: automationAccount\n  name: item.recommendationJobId\n  properties: {\n    schedule: {\n      name: recommendationsScheduleName\n    }\n    runbook: {\n      name: item.runbookName\n    }\n  }\n  dependsOn: [\n    automationSchedules_recommendationsExport\n    automationModule_All\n    automationRunbooks\n  ]\n}]\n\nresource automationJobSchedules_recommendationsIngests 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: recommendationsIngestJobId\n  properties: {\n    schedule: {\n      name: recommendationsIngestScheduleName\n    }\n    runbook: {\n      name: recommendationsIngestRunbookName\n    }\n  }\n  dependsOn: [\n    automationSchedules_recommendationsIngest\n    automationModule_All\n    automationRunbooks\n  ]\n}\n\nresource automationJobSchedules_recommendationsLogAnalyticsIngest 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: recommendationsLogAnalyticsIngestJobId\n  properties: {\n    schedule: {\n      name: recommendationsIngestScheduleName\n    }\n    runbook: {\n      name: recommendationsLogAnalyticsIngestRunbookName\n    }\n  }\n  dependsOn: [\n    automationSchedules_recommendationsIngest\n    automationModule_All\n    automationRunbooks\n  ]\n}\n\nresource automationJobSchedules_suppressionsLogAnalyticsIngest 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: suppressionsLogAnalyticsIngestJobId\n  properties: {\n    schedule: {\n      name: suppressionsIngestScheduleName\n    }\n    runbook: {\n      name: suppressionsLogAnalyticsIngestRunbookName\n    }\n  }\n  dependsOn: [\n    automationSchedules_suppressionsIngest\n    automationModule_All\n    automationRunbooks\n  ]\n}\n\nresource automationJobSchedules_recommendationsCleanUp 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = {\n  parent: automationAccount\n  name: recommendationsCleanUpJobId\n  properties: {\n    schedule: {\n      name: recommendationsCleanUpScheduleName\n    }\n    runbook: {\n      name: cleanUpOlderRecommendationsRunbookName\n    }\n  }\n  dependsOn: [\n    automationSchedules_recommendationsCleanUp\n    automationModule_All\n    automationRunbooks\n  ]\n}\n\nresource contributorRoleAssignmentGuid_resource 'Microsoft.Authorization/roleAssignments@2018-09-01-preview' = {\n  name: contributorRoleAssignmentGuid\n  properties: {\n    roleDefinitionId: roleContributor\n    principalId: reference(automationAccount.id, '2019-06-01', 'Full').identity.principalId\n    principalType: 'ServicePrincipal'\n  }\n}\n\noutput automationPrincipalId string = reference(automationAccount.id, '2019-06-01', 'Full').identity.principalId\n"
  },
  {
    "path": "azuredeploy.bicep",
    "content": "targetScope = 'subscription'\nparam rgName string\nparam readerRoleAssignmentGuid string = guid(subscription().subscriptionId, rgName)\nparam contributorRoleAssignmentGuid string = guid(rgName)\nparam projectLocation string\n\n@description('The base URI where artifacts required by this template are located')\nparam templateLocation string\n\nparam storageAccountName string\nparam automationAccountName string\nparam sqlServerName string\nparam sqlDatabaseName string = 'azureoptimization'\nparam logAnalyticsReuse bool\nparam logAnalyticsWorkspaceName string\nparam logAnalyticsWorkspaceRG string\nparam logAnalyticsRetentionDays int = 120\nparam sqlBackupRetentionDays int = 7\nparam sqlAdminLogin string\n\n@secure()\nparam sqlAdminPassword string\nparam cloudEnvironment string = 'AzureCloud'\nparam authenticationOption string = 'ManagedIdentity'\n\n@description('Base time for all automation runbook schedules.')\nparam baseTime string = utcNow('u')\nparam resourceTags object\n\nparam roleReader string = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'\n\nresource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {\n  name: rgName\n  location: projectLocation\n  tags: resourceTags\n  dependsOn: []\n}\n\nmodule resourcesDeployment './azuredeploy-nested.bicep' = {\n  name: 'resourcesDeployment'\n  scope: resourceGroup(rgName)\n  params: {\n    projectLocation: projectLocation\n    templateLocation: templateLocation\n    storageAccountName: storageAccountName\n    automationAccountName: automationAccountName\n    sqlServerName: sqlServerName\n    sqlDatabaseName: sqlDatabaseName\n    logAnalyticsReuse: logAnalyticsReuse\n    logAnalyticsWorkspaceName: logAnalyticsWorkspaceName\n    logAnalyticsWorkspaceRG: logAnalyticsWorkspaceRG\n    logAnalyticsRetentionDays: logAnalyticsRetentionDays\n    sqlBackupRetentionDays: sqlBackupRetentionDays\n    sqlAdminLogin: sqlAdminLogin\n    sqlAdminPassword: sqlAdminPassword\n    cloudEnvironment: cloudEnvironment\n    authenticationOption: authenticationOption\n    baseTime: baseTime\n    contributorRoleAssignmentGuid: contributorRoleAssignmentGuid\n    resourceTags: resourceTags\n  }\n  dependsOn: [\n    rg\n  ]\n}\n\nresource readerRoleAssignmentGuid_resource 'Microsoft.Authorization/roleAssignments@2018-09-01-preview' = {\n  name: readerRoleAssignmentGuid\n  properties: {\n    roleDefinitionId: roleReader\n    principalId: resourcesDeployment.outputs.automationPrincipalId\n    principalType: 'ServicePrincipal'\n  }\n}\n\noutput automationPrincipalId string = resourcesDeployment.outputs.automationPrincipalId\n"
  },
  {
    "path": "custom-recommendations-types.json",
    "content": "{\n    \"recommendations\": [\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"c320b790-2e58-452a-aa63-7b62c383ad8a\",\n            \"impact\": \"Medium\",\n            \"description\": \"Virtual Machine has been deallocated for long with disks still incurring costs\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"c84d5e86-e2d6-4d62-be7c-cecfbd73b0db\",\n            \"impact\": \"Medium\",\n            \"description\": \"Unattached disks (without owner VM) incur in unnecessary costs\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"dc3d2baa-26c8-435e-aa9d-edb2bfd6fff6\",\n            \"impact\": \"High\",\n            \"description\": \"Application Gateways without a backend pool incur in unnecessary costs\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"f1ed3bb2-3cb5-41e6-ba38-7001d5ff87f5\",\n            \"impact\": \"Medium\",\n            \"description\": \"Standard Load Balancers with rules defined and without a backend pool incur in unnecessary costs\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"ff68f4e5-1197-4be9-8e5f-8760d7863cb4\",\n            \"impact\": \"High\",\n            \"description\": \"Underused SQL Databases (performance capacity waste)\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"3125883f-8b9f-4bde-a0ff-6c739858c6e1\",\n            \"impact\": \"Low\",\n            \"description\": \"Orphaned Public IP (without owner resource) incur in unnecessary costs\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"a4955cc9-533d-46a2-8625-5c4ebd1c30d5\",\n            \"impact\": \"High\",\n            \"description\": \"VM Scale Set has been underutilized\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"4854b5dc-4124-4ade-879e-6a7bb65350ab\",\n            \"impact\": \"High\",\n            \"description\": \"Premium SSD disk has been underutilized\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"08e049ca-18b0-4d22-b174-131a91d0381c\",\n            \"impact\": \"Medium\",\n            \"description\": \"Storage Account without retention policy in place\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"042adaca-ebdf-49b4-bc1b-2800b6e40fea\",\n            \"impact\": \"High\",\n            \"description\": \"Underused App Service Plans (performance capacity waste)\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"ef525225-8b91-47a3-81f3-e674e94564b6\",\n            \"impact\": \"High\",\n            \"description\": \"App Service Plans without any application incur in unnecessary costs\"\n        },\n        {\n            \"category\": \"Cost\",\n            \"typeId\": \"110fea55-a9c3-480d-8248-116f61e139a8\",\n            \"impact\": \"High\",\n            \"description\": \"Virtual Machine is stopped (not deallocated) and still incurring costs\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"255de20b-d5e4-4be5-9695-620b4a905774\",\n            \"impact\": \"High\",\n            \"description\": \"Availability Sets should have a fault domain count of 3 or equal or greater than half of the Virtual Machines count\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"9764e285-2eca-46c5-b49e-649c039cf0cf\",\n            \"impact\": \"High\",\n            \"description\": \"Availability Sets should have an update domain count equal or greater than half of the Virtual Machines count\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"e530029f-9b6a-413a-99ed-81af54502bb9\",\n            \"impact\": \"High\",\n            \"description\": \"Virtual Machines in unmanaged Availability Sets should not share the same Storage Account\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"b70f44fa-5ef9-4180-b2f9-9cc6be07ab3e\",\n            \"impact\": \"Medium\",\n            \"description\": \"Virtual Machines with unmanaged disks should not share the same Storage Account\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"998b50d8-e654-417b-ab20-a31cb11629c0\",\n            \"impact\": \"Medium\",\n            \"description\": \"Virtual Machines should be placed in an Availability Set together with other instances with the same role\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"fe577af5-dfa2-413a-82a9-f183196c1f49\",\n            \"impact\": \"Medium\",\n            \"description\": \"Virtual Machines should not be the only instance in an Availability Set\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"024049e7-f63a-4e1c-b620-f011aafbc576\",\n            \"impact\": \"Medium\",\n            \"description\": \"Each Virtual Machine should have its unmanaged disks stored in a single Storage Account for higher availability and manageability\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"b576a069-b1f2-43a6-9134-5ee75376402a\",\n            \"impact\": \"High\",\n            \"description\": \"Virtual Machines should use Managed Disks for higher availability and manageability\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"1bf03c4a-c402-4e6c-bf20-051b18af30e2\",\n            \"impact\": \"High\",\n            \"description\": \"Virtual Machine Scale Sets should use Managed Disks for higher availability and manageability\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"1a77887c-7375-434e-af19-c2543171e0b8\",\n            \"impact\": \"High\",\n            \"description\": \"Virtual Machines should be placed in multiple Availability Zones\"\n        },\n        {\n            \"category\": \"HighAvailability\",\n            \"typeId\": \"47e5457c-b345-4372-b536-8887fa8f0298\",\n            \"impact\": \"High\",\n            \"description\": \"Virtual Machine Scale Sets should be placed in multiple Availability Zones\"\n        },\n        {\n            \"category\": \"Security\",\n            \"typeId\": \"ecd969c8-3f16-481a-9577-5ed32e5e1a1d\",\n            \"impact\": \"Medium\",\n            \"description\": \"Microsoft Entra application with credentials expiration not set or too far in time\"\n        },\n        {\n            \"category\": \"Security\",\n            \"typeId\": \"b5491cde-f76c-4423-8c4c-89e3558ff2f2\",\n            \"impact\": \"Medium\",\n            \"description\": \"NSG rules referring to empty or inexisting subnets\"\n        },\n        {\n            \"category\": \"Security\",\n            \"typeId\": \"3dc1d1f8-19ef-4572-9c9d-78d62831f55a\",\n            \"impact\": \"Medium\",\n            \"description\": \"NSG rules referring to orphan or inexisting NICs\"\n        },\n        {\n            \"category\": \"Security\",\n            \"typeId\": \"fe40cbe7-bdee-4cce-b072-cf25e1247b7a\",\n            \"impact\": \"High\",\n            \"description\": \"NSG rules referring to orphan or inexisting Public IPs\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"3292c489-2782-498b-aad0-a4cef50f6ca2\",\n            \"impact\": \"Medium\",\n            \"description\": \"Microsoft Entra application with credentials expired or about to expire\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"48619512-f4e6-4241-9c85-16f7c987950c\",\n            \"impact\": \"Medium\",\n            \"description\": \"Load Balancers without a backend pool are useless\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"5292525b-5095-4e52-803e-e17192f1d099\",\n            \"impact\": \"Medium\",\n            \"description\": \"Subnets with a high IP space usage may constrain operations\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"0f27b41c-869a-4563-86e9-d1c94232ba81\",\n            \"impact\": \"Medium\",\n            \"description\": \"Subnets with a low IP space usage are a waste of virtual network address space\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"343bbfb7-5bec-4711-8353-398454d42b7b\",\n            \"impact\": \"Medium\",\n            \"description\": \"Subnets without any IP usage are a waste of virtual network address space\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"4c5c2d0c-b6a4-4c59-bc18-6fff6c1f5b23\",\n            \"impact\": \"Medium\",\n            \"description\": \"Orphaned Network Interfaces (without owner VM or PE) unnecessarily consume IP address space\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"c6a88d8c-3242-44b0-9793-c91897ef68bc\",\n            \"impact\": \"High\",\n            \"description\": \"Subscriptions close to the maximum limit of RBAC assignments\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"b36dea3e-ef21-45a9-a704-6f629fab236d\",\n            \"impact\": \"High\",\n            \"description\": \"Management Groups close to the maximum limit of RBAC assignments\"\n        },\n        {\n            \"category\": \"OperationalExcellence\",\n            \"typeId\": \"4468da8d-1e72-4998-b6d2-3bc38ddd9330\",\n            \"impact\": \"High\",\n            \"description\": \"Subscriptions close to the maximum limit of resource groups\"\n        },\n        {\n            \"category\": \"Performance\",\n            \"typeId\": \"20a40c62-e5c8-4cc3-9fc2-f4ac75013182\",\n            \"impact\": \"Medium\",\n            \"description\": \"VM Scale Set performance has been constrained by lack of resources\"\n        },\n        {\n            \"category\": \"Performance\",\n            \"typeId\": \"724ff2f5-8c83-4105-b00d-029c4560d774\",\n            \"impact\": \"Medium\",\n            \"description\": \"SQL Database performance has been constrained by lack of resources\"\n        },\n        {\n            \"category\": \"Performance\",\n            \"typeId\": \"351574cb-c105-4538-a778-11dfbe4857bf\",\n            \"impact\": \"Medium\",\n            \"description\": \"App Service Plan performance has been constrained by lack of resources\"\n        }\n    ]\n}"
  },
  {
    "path": "docs/configuring-workspaces.md",
    "content": "# Configuring Log Analytics workspaces\n\n## Validating/configuring performance counters collection\n\nIf you want to fully leverage the VM right-size augmented recommendation, you need to have your VMs sending logs to a Log Analytics workspace (it should normally be the one you chose at AOE installation time, but it can be a different one) and you need them to send specific performance counters. The list of required counters is defined [here](../perfcounters.json). The AOE provides a couple of tools that help you validate and fix the configured Log Analytics performance counters, depending on the type of agent you are using to collect logs from your machines.\n\n### Azure Monitor Agent (preferred approach)\n\nWith the help of the [Setup-DataCollectionRules.ps1](./Setup-DataCollectionRules.ps1) script, you can create a couple of Data Collection Rules (DCR) - one per OS type - that you configure to stream performance counters to the Log Analytics workspace of your choice. After creating the DCRs with the script below, you just have to manually or automatically (e.g., with Azure Policy) associate your VMs to the respective DCRs.\n\n#### Requirements\n\n```powershell\nInstall-Module -Name Az.Accounts\nInstall-Module -Name Az.Resources\nInstall-Module -Name Az.OperationalInsights\n```\n\n#### Usage\n\n```powershell\n./Setup-DataCollectionRules.ps1 -DestinationWorkspaceResourceId <Log Analytics workspace ARM resource ID> [-AzureEnvironment <AzureChinaCloud|AzureUSGovernment|AzureCloud>] [-IntervalSeconds <performance counter collection frequency - default 60>] [-ResourceTags <hashtable with the tag name/value pairs to apply to the DCR>]\n\n# Example 1 - create Linux and Windows DCRs with the default options\n./Setup-DataCollectionRules.ps1 -DestinationWorkspaceResourceId \"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.OperationalInsights/workspaces/myWorkspace\"\n\n# Example 2 - create DCRs using a custom counter collection frequency and assigning specific tags\n./Setup-DataCollectionRules.ps1 -DestinationWorkspaceResourceId \"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.OperationalInsights/workspaces/myWorkspace\" -IntervalSeconds 30 -ResourceTags @{\"tagName\"=\"tagValue\";\"otherTagName\"=\"otherTagValue\"}\n```\n\n### Log Analytics agent (legacy Microsoft Monitoring Agent)\n\nWith the help of the [Setup-LogAnalyticsWorkspaces.ps1](./Setup-LogAnalyticsWorkspaces.ps1) script, you can validate and fix the configured Log Analytics performance counters on the workspaces of your choice. In its simplest form of usage, it looks at all the Log Analytics workspaces you have access to and, for each workspace with Azure VMs onboarded, it validates performance counters configuration and tells you which counters are missing. But you can target a specific workspace and, if required, automatically fix the missing counters. See usage details below.\n\n#### Requirements\n\n```powershell\nInstall-Module -Name Az.Accounts\nInstall-Module -Name Az.ResourceGraph\nInstall-Module -Name Az.OperationalInsights\n```\n\n#### Usage\n\n```powershell\n./Setup-LogAnalyticsWorkspaces.ps1 [-AzureEnvironment <AzureChinaCloud|AzureUSGovernment|AzureGermanCloud|AzureCloud>] [-WorkspaceIds <comma-separated list of Log Analytics workspace IDs to validate>] [-IntervalSeconds <performance counter collection frequency - default 60>] [-AutoFix]\n\n# Example 1 - just check all the workspaces configuration\n./Setup-LogAnalyticsWorkspaces.ps1\n\n# Example 2 - fix all workspaces configuration (using default counter collection frequency)\n./Setup-LogAnalyticsWorkspaces.ps1 -AutoFix\n\n# Example 3 - fix specific workspaces configuration, using a custom counter collection frequency\n./Setup-LogAnalyticsWorkspaces.ps1 -AutoFix -WorkspaceIds \"d69e840a-2890-4451-b63c-bcfc5580b90f\",\"961550b2-2c4a-481a-9559-ddf53de4b455\" -IntervalSeconds 30\n```\n\n## Estimating the cost of onboarding additional VMs or adding missing performance metrics\n\nEach performance counter entry in the `Perf` table has different sizings, depending on the [7 required counters](../perfcounters.json) per OS type. The following table enumerates the size (in bytes) per performance counter entry.\n\nOS Type | Object | Counter | Size | Collections per interval/VM\n--- | --- | --- | ---: | --- |\nWindows | Processor | % Processor Time | 200 | 1 + vCPUs count\nWindows | Memory | Available MBytes | 220 | 1\nWindows | LogicalDisk | Disk Read Bytes/sec | 250 | 3 + data disks count\nWindows | LogicalDisk | Disk Write Bytes/sec | 250 | 3 + data disks count\nWindows | LogicalDisk | Disk Reads/sec | 250 | 3 + data disks count\nWindows | LogicalDisk | Disk Writes/sec | 250 | 3 + data disks count\nWindows | Network Adapter | Bytes Total/sec | 290 | network adapters count\nLinux | Processor | % Processor Time | 200\nLinux | Memory | % Used Memory | 200\nLinux | Logical Disk | Disk Read Bytes/sec | 250 | 3 + data disks count\nLinux | Logical Disk | Disk Write Bytes/sec | 250 | 3 + data disks count\nLinux | Logical Disk | Disk Reads/sec | 250 | 3 + data disks count\nLinux | Logical Disk | Disk Writes/sec | 250 | 3 + data disks count\nLinux | Network | Total Bytes | 200 | network adapters count\n\nIn summary, a Windows VM generates, in average, 245 bytes per performance counter entry, while a Linux consumes a bit less, 230 bytes per entry. However, depending on the number of CPU cores, data disks or network adapters, a VM will generate more or less Log Analytics entries. For example, a Windows VM with 4 vCPUs, 1 data disk and 5 network adapters will generate 5 * 200 + 220 + 4 * 250 + 4 * 250 + 4 * 250 + 4 * 250 + 5 * 290 = 6670 bytes (6.5 KB) per collection interval. If you set your Performance Counters interval to 60 seconds, then you'll have 60 * 24 * 30 * 6.5 = 280800 KB (274 MB) of ingestion data per month, which means less than 0.70 EUR/month at the Log Analytics ingestion retail price (Pay As You Go).\n\n## Using multiple Log Analytics workspaces for VM performance metrics\n\nIf you have VMs onboarded to multiple Log Analytics workspaces and you want them to be fully included in the VM right-size recommendations report, you can add those workspaces to the solution just by adding a new variable to the AOE Azure Automation account. In the Automation Account _Shared Resources - Variables_ menu option, click on the _Add a variable button_ and enter `AzureOptimization_RightSizeAdditionalPerfWorkspaces` as the variable name and fill in the comma-separated list of workspace IDs (see example below). Finally, click on _Create_.\n\n![Adding an Automation Account variable with a list of additional workspace IDs for the VM right-size recommendations](./loganalytics-additionalperfworkspaces.jpg \"Additional workspace IDs variable creation\")"
  },
  {
    "path": "docs/customizing-aoe.md",
    "content": "# Customizing the Azure Optimization Engine\n\nThere are many customization options available in AOE, in the form of Azure Automation variables. The list below is a highlight of the most relevant configuration variables. To access them, go to the Automation Account _Shared Resources - Variables_ menu option.\n\n* `AzureOptimization_AdvisorFilter` - If you are not interested in getting recommendations for all the non-Cost Advisor pillars, you can specify a pillar-level filter (comma-separated list with at least one of the following: `HighAvailability,Security,Performance,OperationalExcellence`). Defaults to all pillars.\n* `AzureOptimization_AuthenticationOption` - The default authentication method for Automation Runbooks is `RunAsAccount`. But you can change to `ManagedIdentity` if you're using a Hybrid Worker in an Azure VM.\n* `AzureOptimization_ConsumptionOffsetDays` - The Azure Consumption data collection runbook queries each day for billing events that occurred 7 days ago (default). You can change to a closer offset, but bear in mind that some subscription types (e.g., MSDN) to not support a lower value.\n* `AzureOptimization_PerfPercentileCpu` - The default percentile for CPU metrics aggregations is 99. The lower the percentile, the less conservative will be VM right-size fit score algorithm.\n* `AzureOptimization_PerfPercentileDisk` - The default percentile for disk IO/throughput metrics aggregations is 99. The lower the percentile, the less conservative will be VM right-size fit score algorithm.\n* `AzureOptimization_PerfPercentileMemory` - The default percentile for memory metrics aggregations is 99. The lower the percentile, the less conservative will be VM right-size fit score algorithm.\n* `AzureOptimization_PerfPercentileNetwork` - The default percentile for network metrics aggregations is 99. The lower the percentile, the less conservative will be VM right-size fit score algorithm.\n* `AzureOptimization_PerfPercentileSqlDtu` - The default percentile to be used for SQL DB DTU metrics. The lower the percentile, the less conservative will be the SQL Database right-size algorithm.\n* `AzureOptimization_PerfThresholdCpuPercentage` - The CPU threshold (in % Processor Time) above which the VM right-size fit score will decrease or below which the VM scale set right-size Cost recommendation will trigger.\n* `AzureOptimization_PerfThresholdCpuShutdownPercentage` - The CPU threshold (in % Processor Time) above which the VM right-size fit score will decrease (_shutdown recommendations only_).\n* `AzureOptimization_PerfThresholdCpuDegradedMaxPercentage` - The CPU threshold (Maximum observed in % Processor Time) above which the VM scale set right-size Performance recommendation will trigger.\n* `AzureOptimization_PerfThresholdCpuDegradedAvgPercentage` - The CPU threshold (Average observed in % Processor Time) above which the VM scale set right-size Performance recommendation will trigger.\n* `AzureOptimization_PerfThresholdMemoryPercentage` - The memory threshold (in % Used Memory) above which the VM right-size fit score will decrease or below which the VM scale set right-size Cost recommendation will trigger.\n* `AzureOptimization_PerfThresholdMemoryShutdownPercentage` - The memory threshold (in % Used Memory) above which the VM right-size fit score will decrease (_shutdown recommendations only_).\n* `AzureOptimization_PerfThresholdMemoryDegradedPercentage` - The memory threshold (in % Used Memory) above which the VM scale set right-size Performance recommendation will trigger.\n* `AzureOptimization_PerfThresholdNetworkMbps` - The network threshold (in Total Mbps) above which the VM right-size fit score will decrease.\n* `AzureOptimization_PerfThresholdNetworkShutdownMbps` - The network threshold (in Total Mbps) above which the VM right-size fit score will decrease (_shutdown recommendations only_).\n* `AzureOptimization_PerfThresholdDtuPercentage` - The DTU usage percentage threshold below which a SQL Database instance is considered underutilized.\n* `AzureOptimization_RecommendAdvisorPeriodInDays` - The interval in days to look for Advisor recommendations in the Log Analytics repository - the default is 7, as Advisor recommendations are collected once a week.\n* `AzureOptimization_RecommendationAADMaxCredValidityYears` - The maximum number of years for a Service Principal credential/certificate validity - any validity above this interval will generate a Security recommendation. Defaults to 2.\n* `AzureOptimization_RecommendationAADMinCredValidityDays` - The minimum number of days for a Service Principal credential/certificate before it expires - any validity below this interval will generate an Operational Excellence recommendation. Defaults to 30.\n* `AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays` - The number of consecutive days a VM has been deallocated before being recommended for deletion (_Virtual Machine has been deallocated for long with disks still incurring costs_). Defaults to 30.\n* `AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold` - The maximum percentage tolerated for subnet IP space usage. Defaults to 80.\n* `AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold` - The minimum percentage for subnet IP space usage - any usage below this value will flag the respective subnet as using low IP space. Defaults to 5.\n* `AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays` - The minimum age in days for an empty subnet to be flagged, thus avoiding flagging newly created subnets. Defaults to 30.\n* `AzureOptimization_RecommendationVNetSubnetUsedPercentageExclusions` - Comma-separated, single-quote enclosed list of subnet names that must be excluded from subnet usage percentage recommendations, e.g., 'gatewaysubnet','azurebastionsubnet'. Defaults to 'gatewaysubnet'.\n* `AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold` - The maximum percentage of RBAC assignments limits usage. Defaults to 80.\n* `AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold` - The maximum percentage of Resource Groups count per subscription limits usage. Defaults to 80.\n* `AzureOptimization_RecommendationRBACSubscriptionsAssignmentsLimit` - The maximum limit for RBAC assignments per subscription. Currently set to 2000 (as [documented](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-rbac-limits)).\n* `AzureOptimization_RecommendationRBACMgmtGroupsAssignmentsLimit` - The maximum limit for RBAC assignments per management group. Currently set to 500 (as [documented](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-rbac-limits)).\n* `AzureOptimization_RecommendationResourceGroupsPerSubLimit` - The maximum limit for Resource Group count per subscription. Currently set to 980 (as [documented](https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#subscription-limits)).\n* `AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage` - The minimum Storage Account growth percentage required to flag Storage as not having a retention policy in place.\n* `AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold` - The minimum monthly cost (in your EA/MCA currency) required to flag Storage as not having a retention policy in place.\n* `AzureOptimization_RecommendationStorageAcountGrowthLookbackDays` - The lookback period (in days) for analyzing Storage Account growth.\n* `AzureOptimization_ReferenceRegion` - The Azure region used as a reference for getting the list of available SKUs (defaults to `westeurope`).\n* `AzureOptimization_RemediateRightSizeMinFitScore` - The minimum fit score a VM right-size recommendation must have for the remediation to occur.\n* `AzureOptimization_RemediateRightSizeMinWeeksInARow` - The minimum number of weeks in a row a VM right-size recommendation must have been done for the remediation to occur.\n* `AzureOptimization_RemediateRightSizeTagsFilter` - The tag name/value pairs a VM right-size recommendation must have for the remediation to occur. Example: `[ { \"tagName\": \"a\", \"tagValue\": \"b\" }, { \"tagName\": \"c\", \"tagValue\": \"d\" } ]`\n* `AzureOptimization_RemediateLongDeallocatedVMsMinFitScore` - The minimum fit score a long deallocated VM recommendation must have for the remediation to occur.\n* `AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow` - The minimum number of weeks in a row a long deallocated VM recommendation must have been done for the remediation to occur.\n* `AzureOptimization_RemediateLongDeallocatedVMsTagsFilter` - The tag name/value pairs a long deallocated VM recommendation must have for the remediation to occur. Example: `[ { \"tagName\": \"a\", \"tagValue\": \"b\" }, { \"tagName\": \"c\", \"tagValue\": \"d\" } ]`\n* `AzureOptimization_RemediateUnattachedDisksMinFitScore` - The minimum fit score an unattached disk recommendation must have for the remediation to occur.\n* `AzureOptimization_RemediateUnattachedDisksMinWeeksInARow` - The minimum number of weeks in a row an unattached disk recommendation must have been done for the remediation to occur.\n* `AzureOptimization_RemediateUnattachedDisksAction` - The action to apply for an unattached disk recommendation remediation (`Delete` or `Downsize`).\n* `AzureOptimization_RemediateUnattachedDisksTagsFilter` - The tag name/value pairs an unattached disk recommendation must have for the remediation to occur. Example: `[ { \"tagName\": \"a\", \"tagValue\": \"b\" }, { \"tagName\": \"c\", \"tagValue\": \"d\" } ]`\n* `AzureOptimization_RightSizeAdditionalPerfWorkspaces` - A comma-separated list of additional Log Analytics workspace IDs where to look for VM metrics (see [Configuring Log Analytics workspaces](./configuring-workspaces.md)).\n* `AzureOptimization_PerfThresholdDiskIOPSPercentage` - The disk IOPS usage percentage threshold below which the underutilized Premium SSD disks recommendation will trigger.\n* `AzureOptimization_PerfThresholdDiskMBsPercentage` - The disk throughput usage percentage threshold below which the underutilized Premium SSD disks recommendation will trigger.\n* `AzureOptimization_RecommendationsMaxAgeInDays` - The maximum age (in days) for a recommendation to be kept in the SQL database. Default: 365.\n* `AzureOptimization_RetailPricesCurrencyCode` - The currency code (e.g., EUR, USD, etc.) used to collect the Reservations retail prices.\n* `AzureOptimization_PriceSheetMeterCategories` - The comma-separated meter categories used for Pricesheet filtering, in order to avoid ingesting unnecessary data. Defaults to \"Virtual Machines,Storage\"\n* `AzureOptimization_ConsumptionScope` - The scope of the consumption exports: `Subscription` (default) or `BillingAccount`. See [more details](#enabling-the-reservations-and-benefits-usage-workbooks).\n"
  },
  {
    "path": "docs/suppressing-recommendations.md",
    "content": "# Suppressing recommendations\n\nWhen working on the recommendations provided by AOE, you may find some cases where the recommendation does not apply for some reason. For example, AOE is suggesting high availability recommendations that do not apply to Dev/Test Virtual Machines, or recommending enabling Azure Backup for non-critical VMs. You can suppress recommendations in two ways:\n\n* If recommendations are originated from Azure Advisor, you can simply go to the Azure Portal and [dismiss/postpone the recommendation](https://docs.microsoft.com/en-us/azure/advisor/view-recommendations#dismissing-and-postponing-recommendations).\n* If recommendations are custom to AOE or using the Azure Advisor interface is not viable, you can suppress them in AOE using the [Suppress-Recommendation.ps1](../Suppress-Recommendation.ps1) helper script (see instructions below).\n\n## Identifying the recommendation to suppress\n\nIn the Power BI report, if you drill through the details of a recommendation (Rec. Details page), you will find the Recommendation Id in the header. Copy this Id, by using the \"Copy value\" right-click menu option. You'll need this ID to call the Supress-Recommendation.ps1 script.\n\n![Copying the Recommendation Id value from the Recommendation Details page in the Power BI report](./powerbi-recdetails-recommendationid.jpg \"Copy the Recommendation Id value\")\n\n## Supressing the recommendation\n\nFrom a PowerShell prompt, call the [Suppress-Recommendation.ps1](../Suppress-Recommendation.ps1) script as follows:\n\n```powershell\n./Suppress-Recommendation.ps1 -RecommendationId <recommendation Id>\n\n# Example\n./Suppress-Recommendation.ps1 -RecommendationId A2824017-602C-47DF-860D-B0B5A8CA7768\n```\n\nThe script will ask you for the Azure SQL Server hostname, database and user credentials. After successfully finding the recommendation in the AOE database, it will ask you about the type of suppression:\n\n* **Exclude** - this recommendation type will be completely excluded from the engine and will no longer be generated for any resource\n* **Dismiss** - this recommendation will be dismissed for the scope to be chosen next (instance, resource group or subscription)\n* **Snooze** - this recommendation will be postponed for the duration (in days) and scope to be chosen next (instance, resource group or subscription)\n\nDepending on the type of suppression chosen, you can be asked to provide the suppression scope (subscription, resource group or resource instance) or the suppression duration (for Snooze suppressions). Finally, you should identify the author and the reason for the suppression."
  },
  {
    "path": "model/filters-table.sql",
    "content": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[Filters]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\n\tBEGIN\n\t\tCREATE TABLE [dbo].[Filters](\n\t\t\t[FilterId] [uniqueidentifier] NOT NULL DEFAULT NEWID(),\n\t\t\t[RecommendationSubTypeId] [uniqueidentifier] NOT NULL,\n\t\t\t[FilterType] [varchar](20) NOT NULL,\n\t\t\t[InstanceId] [varchar](1000) NULL,\n\t\t\t[FilterStartDate] [datetime] NOT NULL,\n\t\t\t[FilterEndDate] [datetime] NULL,\n\t\t\t[Author] [varchar](50) NULL,\n\t\t\t[Notes] [nvarchar](max) NULL,\n\t\t\t[IsEnabled] [bit] NOT NULL\n\t\t)\n\n\t\tALTER TABLE [dbo].[Filters] ADD PRIMARY KEY CLUSTERED \n\t\t(\n\t\t\t[FilterId] ASC\n\t\t)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]\n\tEND\n"
  },
  {
    "path": "model/loganalyticsingestcontrol-initialize.sql",
    "content": "IF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argvmexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argvmexports', '1901-01-01T00:00:00Z', -1, 'VMsV1', 'ARGVirtualMachine')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argdiskexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argdiskexports', '1901-01-01T00:00:00Z', -1, 'DisksV1', 'ARGManagedDisk')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argvhdexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argvhdexports', '1901-01-01T00:00:00Z', -1, 'VhdDisksV1', 'ARGUnmanagedDisk')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argavailsetexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argavailsetexports', '1901-01-01T00:00:00Z', -1, 'AvailSetsV1', 'ARGAvailabilitySet')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'advisorexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('advisorexports', '1901-01-01T00:00:00Z', -1, 'AdvisorV1', 'AzureAdvisor')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'remediationlogs')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('remediationlogs', '1901-01-01T00:00:00Z', -1, 'RemediationV1', 'RemediationLogs')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'consumptionexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('consumptionexports', '1901-01-01T00:00:00Z', -1, 'ConsumptionV1', 'AzureConsumption')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'aadobjectsexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('aadobjectsexports', '1901-01-01T00:00:00Z', -1, 'AADObjectsV1', 'AADObjects')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'arglbexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('arglbexports', '1901-01-01T00:00:00Z', -1, 'LoadBalancersV1', 'ARGLoadBalancer')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argappgwexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argappgwexports', '1901-01-01T00:00:00Z', -1, 'AppGatewaysV1', 'ARGAppGateway')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argrescontainersexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argrescontainersexports', '1901-01-01T00:00:00Z', -1, 'ResourceContainersV1', 'ARGResourceContainers')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'rbacexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('rbacexports', '1901-01-01T00:00:00Z', -1, 'RBACAssignmentsV1', 'RBACAssignments')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argvnetexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argvnetexports', '1901-01-01T00:00:00Z', -1, 'VNetsV1', 'ARGVirtualNetwork')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argnicexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argnicexports', '1901-01-01T00:00:00Z', -1, 'NICsV1', 'ARGNetworkInterface')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argnsgexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argnsgexports', '1901-01-01T00:00:00Z', -1, 'NSGsV1', 'ARGNSGRule')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argpublicipexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argpublicipexports', '1901-01-01T00:00:00Z', -1, 'PublicIPsV1', 'ARGPublicIP')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argvmssexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argvmssexports', '1901-01-01T00:00:00Z', -1, 'VMSSV1', 'ARGVMSS')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argsqldbexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argsqldbexports', '1901-01-01T00:00:00Z', -1, 'SqlDbV1', 'ARGSqlDb')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'azmonitorexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('azmonitorexports', '1901-01-01T00:00:00Z', -1, 'MonitorMetricsV1', 'MonitorMetrics')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'policystateexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('policystateexports', '1901-01-01T00:00:00Z', -1, 'PolicyStatesV1', 'PolicyStates')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'recommendationsexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('recommendationsexports', '2022-12-26T00:00:00Z', -1, 'RecommendationsV1', 'Recommendations')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'reservationsexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('reservationsexports', '1901-01-01T00:00:00Z', -1, 'ReservationsUsageV1', 'ReservationsUsage')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'argappserviceplanexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('argappserviceplanexports', '1901-01-01T00:00:00Z', -1, 'AppServicePlansV1', 'AppServicePlans')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'pricesheetexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('pricesheetexports', '1901-01-01T00:00:00Z', -1, 'PricesheetV1', 'Pricesheet')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'reservationspriceexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('reservationspriceexports', '1901-01-01T00:00:00Z', -1, 'ReservationsPriceV1', 'ReservationsPrice')\nEND\n\nIF NOT EXISTS (SELECT * FROM [dbo].[LogAnalyticsIngestControl] WHERE StorageContainerName = 'savingsplansexports')\nBEGIN\n    INSERT INTO [dbo].[LogAnalyticsIngestControl] \n    VALUES ('savingsplansexports', '1901-01-01T00:00:00Z', -1, 'SavingsPlansUsageV1', 'SavingsPlansUsage')\nEND\n"
  },
  {
    "path": "model/loganalyticsingestcontrol-table.sql",
    "content": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[LogAnalyticsIngestControl]')\nAND OBJECTPROPERTY(id, N'IsUserTable') = 1)\n\tBEGIN\n\t\tCREATE TABLE [dbo].[LogAnalyticsIngestControl](\n\t\t\t[StorageContainerName] [varchar](50) NOT NULL,\n\t\t\t[LastProcessedDateTime] [datetime] NULL,\n\t\t\t[LastProcessedLine] [int] NULL,\n\t\t\t[LogAnalyticsSuffix] [varchar](50) NOT NULL,\n\t\t\t[CollectedType] [varchar](50) NULL\n\t\t)\n\n\t\tALTER TABLE [dbo].[LogAnalyticsIngestControl] ADD PRIMARY KEY CLUSTERED \n\t\t(\n\t\t\t[StorageContainerName] ASC\n\t\t)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]\n\tEND\nELSE\n\tBEGIN\n\t\tIF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[LogAnalyticsIngestControl]') AND name = 'CollectedType'\n)\t\tBEGIN\n\t\t\tALTER TABLE [dbo].[LogAnalyticsIngestControl] ADD [CollectedType] VARCHAR (50) NULL\n\t\tEND\n\tEND"
  },
  {
    "path": "model/loganalyticsingestcontrol-upgrade.sql",
    "content": "UPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'ARGVirtualMachine' WHERE StorageContainerName = 'argvmexports'\nUPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'ARGManagedDisk' WHERE StorageContainerName = 'argdiskexports'\nUPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'AzureAdvisor' WHERE StorageContainerName = 'advisorexports'\nUPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'RemediationLogs' WHERE StorageContainerName = 'remediationlogs'\nUPDATE [dbo].[LogAnalyticsIngestControl] SET CollectedType = 'AzureConsumption' WHERE StorageContainerName = 'consumptionexports'"
  },
  {
    "path": "model/recommendations-sp.sql",
    "content": "IF OBJECT_ID ( N'[dbo].[GetRecommendations]', 'P' ) IS NOT NULL\nBEGIN\n    DROP PROCEDURE dbo.GetRecommendations\nEND\nEXEC('CREATE PROCEDURE dbo.GetRecommendations   \n    AS BEGIN\n        SET NOCOUNT ON;  \n        SELECT * FROM [dbo].[Recommendations] R\n        WHERE GeneratedDate > GETDATE()-365 AND NOT EXISTS (\n            SELECT * FROM [dbo].[Filters]\n            WHERE FilterType IN (''Snooze'', ''Dismiss'') AND \n                  IsEnabled = 1 AND \n                  R.GeneratedDate > FilterStartDate AND\n                  (FilterEndDate IS NULL OR FilterEndDate > GETDATE()) AND \n                  RecommendationSubTypeId = R.RecommendationSubTypeId AND \n                  (InstanceId IS NULL OR R.InstanceId LIKE ''%'' + InstanceId + ''%'')\n        )  \n    END\n')\n"
  },
  {
    "path": "model/recommendations-table.sql",
    "content": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[Recommendations]') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\n\tBEGIN\n\t\tCREATE TABLE [dbo].[Recommendations](\n\t\t\t[RecommendationId] [uniqueidentifier] NOT NULL DEFAULT NEWID(),\n\t\t\t[GeneratedDate] [datetime] NOT NULL,\n\t\t\t[Cloud] [varchar](20) NOT NULL,\n\t\t\t[Category] [varchar](50) NOT NULL,\n\t\t\t[ImpactedArea] [varchar](300) NOT NULL,\n\t\t\t[Impact] [varchar](20) NOT NULL,\n\t\t\t[RecommendationType] [varchar](50) NOT NULL,\n\t\t\t[RecommendationSubType] [varchar](50) NOT NULL,\n\t\t\t[RecommendationSubTypeId] [uniqueidentifier] NOT NULL,\n\t\t\t[RecommendationDescription] [nvarchar](1000) NULL,\n\t\t\t[RecommendationAction] [nvarchar](1000) NULL,\n\t\t\t[InstanceId] [varchar](1000) NULL,\n\t\t\t[InstanceName] [varchar](500) NULL,\n\t\t\t[AdditionalInfo] [nvarchar](max) NULL,\n\t\t\t[ResourceGroup] [varchar](200) NULL,\n\t\t\t[SubscriptionGuid] [varchar](50) NULL,\n\t\t\t[SubscriptionName] [varchar](250) NULL,\n\t\t\t[TenantGuid] [varchar](50) NULL,\n\t\t\t[FitScore] [real] NOT NULL,\n\t\t\t[Tags] [nvarchar](max) NULL,\n\t\t\t[DetailsUrl] [nvarchar](max) NULL\n\t\t)\n\n\t\tALTER TABLE [dbo].[Recommendations] ADD PRIMARY KEY CLUSTERED \n\t\t(\n\t\t\t[RecommendationId] ASC\n\t\t)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]\n\n\t\tCREATE INDEX IXC_Recommendations_SubTypeId ON [dbo].[Recommendations](RecommendationSubTypeId)\n\n\t\tCREATE INDEX IXC_Recommendations_GeneratedDate ON [dbo].[Recommendations](GeneratedDate)\n\tEND\nELSE\n\tBEGIN\n\t\tALTER TABLE [dbo].[Recommendations] ALTER COLUMN [RecommendationAction] VARCHAR (1000) NULL\n\t\tALTER TABLE [dbo].[Recommendations] ALTER COLUMN [InstanceId] VARCHAR (1000) NULL\n\t\tALTER TABLE [dbo].[Recommendations] ALTER COLUMN [InstanceName] VARCHAR (500) NULL\n\t\tALTER TABLE [dbo].[Recommendations] ALTER COLUMN [ResourceGroup] VARCHAR (200) NULL\n\t\tALTER TABLE [dbo].[Recommendations] ALTER COLUMN [ImpactedArea] VARCHAR (300) NULL\n\t\tIF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Recommendations]') AND name = 'FitScore')\n\t\tBEGIN\n\t\t\tEXEC sp_rename '[dbo].[Recommendations].ConfidenceScore', 'FitScore', 'COLUMN'\n\t\tEND\n\t\tIF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Recommendations]') AND name = 'SubscriptionName')\n\t\tBEGIN\n\t\t\tALTER TABLE [dbo].[Recommendations] ADD [SubscriptionName] VARCHAR (250) NULL\n\t\tEND\n\t\tIF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[dbo].[Recommendations]') AND name = 'TenantGuid')\n\t\tBEGIN\n\t\t\tALTER TABLE [dbo].[Recommendations] ADD [TenantGuid] VARCHAR (50) NULL\n\t\tEND\n\t\tIF NOT EXISTS (SELECT * from sysindexes WHERE id=object_id('Recommendations') and name='IXC_Recommendations_SubTypeId')\n\t\tBEGIN\n\t\t\tCREATE INDEX IXC_Recommendations_SubTypeId ON [dbo].[Recommendations](RecommendationSubTypeId)\n\t\tEND\n\t\tIF NOT EXISTS (SELECT * from sysindexes WHERE id=object_id('Recommendations') and name='IXC_Recommendations_GeneratedDate')\n\t\tBEGIN\n\t\t\tCREATE INDEX IXC_Recommendations_GeneratedDate ON [dbo].[Recommendations](GeneratedDate)\n\t\tEND\n\tEND"
  },
  {
    "path": "model/sqlserveringestcontrol-initialize.sql",
    "content": "IF NOT EXISTS (SELECT * FROM [dbo].[SqlServerIngestControl] WHERE StorageContainerName = 'recommendationsexports')\nBEGIN\n    INSERT INTO [dbo].[SqlServerIngestControl] \n    VALUES \n        ('recommendationsexports', '1901-01-01T00:00:00Z', -1, 'Recommendations')\nEND"
  },
  {
    "path": "model/sqlserveringestcontrol-table.sql",
    "content": "SET ANSI_NULLS ON\nSET QUOTED_IDENTIFIER ON\nIF NOT EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'[dbo].[SqlServerIngestControl]')\nAND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nBEGIN\n\tCREATE TABLE [dbo].[SqlServerIngestControl](\n\t\t[StorageContainerName] [varchar](50) NOT NULL,\n\t\t[LastProcessedDateTime] [datetime] NULL,\n\t\t[LastProcessedLine] [int] NULL,\n\t\t[SqlTableName] [varchar](50) NOT NULL\n\t)\n\tALTER TABLE [dbo].[SqlServerIngestControl] ADD PRIMARY KEY CLUSTERED \n\t(\n\t\t[StorageContainerName] ASC\n\t)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]\nEND"
  },
  {
    "path": "perfcounters.json",
    "content": "[\n    {\n        \"objectName\": \"LogicalDisk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Read Bytes/sec\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"LogicalDisk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Reads/sec\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"LogicalDisk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Write Bytes/sec\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"LogicalDisk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Writes/sec\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"Memory\",\n        \"instance\": \"*\",\n        \"counterName\": \"Available MBytes\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"Network Adapter\",\n        \"instance\": \"*\",\n        \"counterName\": \"Bytes Total/sec\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"Processor\",\n        \"instance\": \"*\",\n        \"counterName\": \"% Processor Time\",\n        \"osType\": \"Windows\"\n    },\n    {\n        \"objectName\": \"Logical Disk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Read Bytes/sec\",\n        \"osType\": \"Linux\"\n    },\n    {\n        \"objectName\": \"Logical Disk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Reads/sec\",\n        \"osType\": \"Linux\"\n    },\n    {\n        \"objectName\": \"Logical Disk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Write Bytes/sec\",\n        \"osType\": \"Linux\"\n    },\n    {\n        \"objectName\": \"Logical Disk\",\n        \"instance\": \"*\",\n        \"counterName\": \"Disk Writes/sec\",\n        \"osType\": \"Linux\"\n    },\n    {\n        \"objectName\": \"Memory\",\n        \"instance\": \"*\",\n        \"counterName\": \"% Used Memory\",\n        \"osType\": \"Linux\"\n    },\n    {\n        \"objectName\": \"Network\",\n        \"instance\": \"*\",\n        \"counterName\": \"Total Bytes\",\n        \"osType\": \"Linux\"\n    },\n    {\n        \"objectName\": \"Processor\",\n        \"instance\": \"*\",\n        \"counterName\": \"% Processor Time\",\n        \"osType\": \"Linux\"\n    }\n]"
  },
  {
    "path": "queries/rbac-spns-keys-expiring.kql",
    "content": "let expiryInterval = 30d;\nlet AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet AppsAndKeys = materialize (AADObjectsTable\n| where ObjectType_s in ('Application','ServicePrincipal')\n| where ObjectSubType_s != 'ManagedIdentity'\n| where Keys_s startswith '['\n| extend Keys = parse_json(Keys_s)\n| project-away Keys_s\n| mv-expand Keys\n| evaluate bag_unpack(Keys)\n| union ( \n    AADObjectsTable\n    | where ObjectType_s in ('Application','ServicePrincipal')\n    | where ObjectSubType_s != 'ManagedIdentity'\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\n    | extend Keys = parse_json(Keys_s)\n    | project-away Keys_s\n    | evaluate bag_unpack(Keys)\n)\n);\nlet ExpirationInRisk = AppsAndKeys\n| where EndDate < now()+expiryInterval and EndDate > now()\n| project ApplicationId_g, KeyId, RiskDate = EndDate;\nlet NotInRisk = AppsAndKeys\n| where EndDate > now()+expiryInterval\n| project ApplicationId_g, KeyId, ComfortDate = EndDate;\nlet ApplicationsInRisk = ExpirationInRisk\n| join kind=leftouter ( NotInRisk ) on ApplicationId_g\n| where isempty(ComfortDate)\n| summarize ExpiresOn = max(RiskDate) by ApplicationId_g;\nlet ServicePrincipals = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'ServicePrincipal'\n| project SPNId = ObjectId_g, ApplicationId_g, PrincipalNames_s, DisplayName_s);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectAssignments = RBACAssignmentsTable\n| join kind=inner (\n    ServicePrincipals\n) on $left.PrincipalId_g == $right.SPNId\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\nlet GroupAssignments = RBACAssignmentsTable\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        ServicePrincipals\n    ) on $left.GroupMember == $right.SPNId\n) on $left.PrincipalId_g == $right.GroupId\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\nAppsAndKeys\n| join kind=inner (ApplicationsInRisk) on ApplicationId_g\n| summarize ExpiresOn = max(EndDate) by ApplicationId_g, DisplayName_s, Cloud_s, KeyType, TenantGuid_g\n| join kind=inner (\n    GroupAssignments\n    | union DirectAssignments\n) on ApplicationId_g\n| distinct DisplayName_s, ExpiresOn, KeyType, RoleDefinition_s, Scope_s, Model_s, Cloud_s, TenantGuid_g\n| order by ExpiresOn asc"
  },
  {
    "path": "queries/rbac-spns-roles-aad-all.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet AppsAndKeys = materialize (AADObjectsTable\n| where ObjectType_s in ('Application','ServicePrincipal')\n| where Keys_s startswith '['\n| extend Keys = parse_json(Keys_s)\n| project-away Keys_s\n| mv-expand Keys\n| evaluate bag_unpack(Keys)\n| union ( \n    AADObjectsTable\n    | where ObjectType_s in ('Application','ServicePrincipal')\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\n    | extend Keys = parse_json(Keys_s)\n    | project-away Keys_s\n    | evaluate bag_unpack(Keys)\n)\n);\nlet ServicePrincipals = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'ServicePrincipal'\n| join kind=inner ( \n    AppsAndKeys\n) on ApplicationId_g\n| project SPNId = ObjectId_g, PrincipalNames_s, DisplayName_s, KeyType, EndDate);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureAD'\n| join kind=inner (\n    ServicePrincipals\n) on $left.PrincipalId_g == $right.SPNId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', KeyType, EndDate, Model_s, TenantGuid_g;\nlet GroupAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureAD'\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        ServicePrincipals\n    ) on $left.GroupMember == $right.SPNId\n) on $left.PrincipalId_g == $right.GroupId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), KeyType, EndDate, Model_s, TenantGuid_g;\nGroupAssignments\n| union DirectAssignments\n| distinct DisplayName_s, RoleDefinition_s, Model_s, Scope_s, Assignment, KeyType, EndDate, PrincipalNames_s, TenantGuid_g\n| where EndDate > now()\n| order by DisplayName_s asc"
  },
  {
    "path": "queries/rbac-spns-roles-arm-all.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet AppsAndKeys = materialize (AADObjectsTable\n| where ObjectType_s in ('Application','ServicePrincipal')\n| where Keys_s startswith '['\n| extend Keys = parse_json(Keys_s)\n| project-away Keys_s\n| mv-expand Keys\n| evaluate bag_unpack(Keys)\n| union ( \n    AADObjectsTable\n    | where ObjectType_s in ('Application','ServicePrincipal')\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\n    | extend Keys = parse_json(Keys_s)\n    | project-away Keys_s\n    | evaluate bag_unpack(Keys)\n)\n);\nlet ServicePrincipals = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'ServicePrincipal'\n| join kind=inner ( \n    AppsAndKeys\n) on ApplicationId_g\n| project SPNId = ObjectId_g, PrincipalNames_s, DisplayName_s, KeyType, EndDate);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureRM'\n| join kind=inner (\n    ServicePrincipals\n) on $left.PrincipalId_g == $right.SPNId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', KeyType, EndDate, Model_s, TenantGuid_g;\nlet GroupAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureRM'\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        ServicePrincipals\n    ) on $left.GroupMember == $right.SPNId\n) on $left.PrincipalId_g == $right.GroupId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), KeyType, EndDate, Model_s, TenantGuid_g;\nGroupAssignments\n| union DirectAssignments\n| distinct DisplayName_s, RoleDefinition_s, Model_s, Scope_s, Assignment, KeyType, EndDate, PrincipalNames_s, TenantGuid_g\n| where EndDate > now()\n| order by DisplayName_s asc"
  },
  {
    "path": "queries/rbac-spns-roles-arm-privileged.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet PrivilegedRoles = dynamic(['Owner','Contributor','Global Administrator', 'Privileged Role Administrator', 'User Access Administrator','Exchange Administrator']);\nlet AppsAndKeys = materialize (AADObjectsTable\n| where ObjectType_s in ('Application','ServicePrincipal')\n| where Keys_s startswith '['\n| extend Keys = parse_json(Keys_s)\n| project-away Keys_s\n| mv-expand Keys\n| evaluate bag_unpack(Keys)\n| union ( \n    AADObjectsTable\n    | where ObjectType_s in ('Application','ServicePrincipal')\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\n    | extend Keys = parse_json(Keys_s)\n    | project-away Keys_s\n    | evaluate bag_unpack(Keys)\n)\n);\nlet ServicePrincipals = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'ServicePrincipal'\n| join kind=inner ( \n    AppsAndKeys\n) on ApplicationId_g\n| project SPNId = ObjectId_g, PrincipalNames_s, DisplayName_s, KeyType, EndDate);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureRM'\n| where RoleDefinition_s in (PrivilegedRoles) and Scope_s !has 'resourcegroups'\n| join kind=inner (\n    ServicePrincipals\n) on $left.PrincipalId_g == $right.SPNId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', KeyType, EndDate, Model_s, TenantGuid_g;\nlet GroupAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureRM'\n| where RoleDefinition_s in (PrivilegedRoles) and Scope_s !has 'resourcegroups'\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        ServicePrincipals\n    ) on $left.GroupMember == $right.SPNId\n) on $left.PrincipalId_g == $right.GroupId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), KeyType, EndDate, Model_s, TenantGuid_g;\nGroupAssignments\n| union DirectAssignments\n| distinct DisplayName_s, RoleDefinition_s, Model_s, Scope_s, Assignment, KeyType, EndDate, PrincipalNames_s, TenantGuid_g\n| where EndDate > now()\n| order by DisplayName_s asc"
  },
  {
    "path": "queries/rbac-users-roles-aad-all.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet EnabledUsers = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'User' and SecurityEnabled_s == 'True'\n| project UserId = ObjectId_g, PrincipalNames_s, DisplayName_s);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectUserAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureAD'\n| join kind=inner (\n    EnabledUsers\n) on $left.PrincipalId_g == $right.UserId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\nlet GroupUserAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureAD'\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        EnabledUsers\n    ) on $left.GroupMember == $right.UserId\n) on $left.PrincipalId_g == $right.GroupId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\nGroupUserAssignments\n| union DirectUserAssignments\n| distinct DisplayName_s, PrincipalNames_s, RoleDefinition_s, Model_s, Scope_s, Assignment, TenantGuid_g\n| order by PrincipalNames_s asc"
  },
  {
    "path": "queries/rbac-users-roles-arm-privileged.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet PrivilegedRoles = dynamic(['Owner','Contributor']);\nlet EnabledUsers = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'User' and SecurityEnabled_s == 'True'\n| project UserId = ObjectId_g, PrincipalNames_s, DisplayName_s);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectUserAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureRM'\n| where RoleDefinition_s in (PrivilegedRoles) and Scope_s !has 'resourcegroups'\n| join kind=inner (\n    EnabledUsers\n) on $left.PrincipalId_g == $right.UserId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\nlet GroupUserAssignments = RBACAssignmentsTable\n| where Model_s == 'AzureRM'\n| where RoleDefinition_s in (PrivilegedRoles) and Scope_s !has 'resourcegroups'\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        EnabledUsers\n    ) on $left.GroupMember == $right.UserId\n) on $left.PrincipalId_g == $right.GroupId\n| project PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\nGroupUserAssignments\n| union DirectUserAssignments\n| distinct DisplayName_s, PrincipalNames_s, RoleDefinition_s, Model_s, Scope_s, Assignment, TenantGuid_g\n| order by PrincipalNames_s asc"
  },
  {
    "path": "queries/rbac-users-roles-guests-privileged.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet PrivilegedRoles = dynamic(['Owner','Contributor','Global Administrator', 'Privileged Role Administrator', 'User Access Administrator','Exchange Administrator']);\nlet EnabledGuestUsers = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'User' and ObjectSubType_s == 'Guest' and SecurityEnabled_s == 'True'\n| project UserId = ObjectId_g, PrincipalNames_s, DisplayName_s);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectUserAssignments = RBACAssignmentsTable\n| where RoleDefinition_s in (PrivilegedRoles) and Scope_s !has 'resourcegroups'\n| join kind=inner (\n    EnabledGuestUsers\n) on $left.PrincipalId_g == $right.UserId\n| project DisplayName_s, PrincipalNames_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\nlet GroupUserAssignments = RBACAssignmentsTable\n| where RoleDefinition_s in (PrivilegedRoles) and Scope_s !has 'resourcegroups'\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        EnabledGuestUsers\n    ) on $left.GroupMember == $right.UserId\n) on $left.PrincipalId_g == $right.GroupId\n| project DisplayName_s, PrincipalNames_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\nGroupUserAssignments\n| union DirectUserAssignments\n| distinct DisplayName_s, PrincipalNames_s, RoleDefinition_s, Model_s, Scope_s, Assignment, TenantGuid_g\n| order by PrincipalNames_s asc"
  },
  {
    "path": "queries/rbac-users-roles-guests.kql",
    "content": "let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d));\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d));\nlet EnabledGuestUsers = materialize(AADObjectsTable\n| where isnotempty(ObjectId_g)\n| where ObjectType_s == 'User' and ObjectSubType_s == 'Guest' and SecurityEnabled_s == 'True'\n| project UserId = ObjectId_g, PrincipalNames_s, DisplayName_s);\nlet GroupMemberships = AADObjectsTable\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n| where PrincipalNames_s startswith '['\n| extend GroupMember = parse_json(PrincipalNames_s)\n| project-away PrincipalNames_s\n| mv-expand GroupMember\n| union (\n    AADObjectsTable\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\n    | extend GroupMember = parse_json(PrincipalNames_s)\n    | project-away PrincipalNames_s\n)\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\nlet DirectUserAssignments = RBACAssignmentsTable\n| join kind=inner (\n    EnabledGuestUsers\n) on $left.PrincipalId_g == $right.UserId\n| project DisplayName_s, PrincipalNames_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\nlet GroupUserAssignments = RBACAssignmentsTable\n| join kind=inner (\n    GroupMemberships\n    | join kind=inner ( \n        EnabledGuestUsers\n    ) on $left.GroupMember == $right.UserId\n) on $left.PrincipalId_g == $right.GroupId\n| project DisplayName_s, PrincipalNames_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\nGroupUserAssignments\n| union DirectUserAssignments\n| distinct DisplayName_s, PrincipalNames_s, RoleDefinition_s, Model_s, Scope_s, Assignment, TenantGuid_g\n| order by PrincipalNames_s asc"
  },
  {
    "path": "runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName,\n\n    [Parameter(Mandatory = $false)]\n    [string] $groupFilter,\n\n    [Parameter(Mandatory = $false)]\n    [string] $userFilter\n)\n\n$ErrorActionPreference = \"Stop\"\n\nfunction Build-CredObjectWithDates {\n    param (\n        [object] $appObject\n    )\n    \n    $credObjects = @()\n\n    foreach ($obj in $appObject.KeyCredentials)\n    {\n        $credObject = New-Object PSObject -Property @{\n            DisplayName = $obj.DisplayName\n            KeyId = $obj.KeyId\n            KeyType = $obj.Type\n            StartDate = (Get-Date($obj.StartDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n            EndDate = (Get-Date($obj.EndDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $credObjects += $credObject        \n    }\n\n    foreach ($obj in $appObject.PasswordCredentials)\n    {\n        $credObject = New-Object PSObject -Property @{\n            DisplayName = $obj.DisplayName\n            KeyId = $obj.KeyId\n            KeyType = \"Password\"\n            StartDate = (Get-Date($obj.StartDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n            EndDate = (Get-Date($obj.EndDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $credObjects += $credObject        \n    }\n\n    return $credObjects\n}\n\nfunction Build-PrincipalNames {\n    param (\n        [object] $appObject\n    )\n    \n    $principalNames = @()\n\n    if ($appObject.Web.HomePageUrl)\n    {\n        $principalNames += $appObject.Web.HomePageUrl\n    }\n\n    foreach ($obj in $appObject.IdentifierUris)\n    {\n        $principalNames += $obj\n    }\n\n    foreach ($obj in $appObject.ServicePrincipalNames)\n    {\n        $principalNames += $obj\n    }\n\n    foreach ($obj in $appObject.AlternativeNames)\n    {\n        $principalNames += $obj\n    }\n\n    return $principalNames\n}\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_AADObjectsContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"aadobjectsexports\"\n}\n\n# Application,ServicePrincipal,User,Group\n$aadObjectsFilter = Get-AutomationVariable -Name  \"AzureOptimization_AADObjectsFilter\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($aadObjectsFilter))\n{\n    $aadObjectsFilter = \"Application,ServicePrincipal\"\n}\n\n$groupFilterVariable = Get-AutomationVariable -Name  \"AzureOptimization_AADObjectsGroupFilter\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($groupFilter) -and -not([string]::IsNullOrEmpty($groupFilterVariable)))\n{\n    $groupFilter = $groupFilterVariable\n}\n\n$userFilterVariable = Get-AutomationVariable -Name  \"AzureOptimization_AADObjectsUserFilter\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($userFilter) -and -not([string]::IsNullOrEmpty($userFilterVariable)))\n{\n    $userFilter = $userFilterVariable\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n#workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888\n$localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)\nif (-not(get-item \"$localPath\\.graph\\\" -ErrorAction SilentlyContinue))\n{\n    New-Item -Type Directory \"$localPath\\.graph\"\n}\n\nImport-Module Microsoft.Graph.Authentication\nImport-Module Microsoft.Graph.Users\nImport-Module Microsoft.Graph.Applications\nImport-Module Microsoft.Graph.Groups\n\nswitch ($cloudEnvironment) {\n    \"AzureUSGovernment\" {  \n        $graphEnvironment = \"USGov\"\n        break\n    }\n    \"AzureChinaCloud\" {  \n        $graphEnvironment = \"China\"\n        break\n    }\n    \"AzureGermanCloud\" {  \n        $graphEnvironment = \"Germany\"\n        break\n    }\n    Default {\n        $graphEnvironment = \"Global\"\n    }\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Microsoft Graph with $externalCredentialName external credential...\"\n    Connect-MgGraph -TenantId $externalTenantId -ClientSecretCredential $externalCredential -Environment $graphEnvironment -NoWelcome\n}\nelse\n{\n    \"Logging in to Microsoft Graph...\"\n    Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome\n}\n    \n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$aadObjectsTypes = $aadObjectsFilter.Split(\",\")\n\n$fileDate = $datetime.ToString(\"yyyyMMdd\")\n\nif (\"Application\" -in $aadObjectsTypes)\n{\n    $aadObjects = @()\n\n    \"Getting AAD applications...\"\n    $apps = Get-MgApplication -All -ExpandProperty Owners -Property Id,AppId,CreatedDateTime,DeletedDateTime,DisplayName,KeyCredentials,PasswordCredentials,Owners,PublisherDomain,Web,IdentifierUris\n    \"Found $($apps.Count) AAD applications\"\n\n    foreach ($app in $apps)\n    {\n        $owners = $null\n        if ($app.Owners.Count -gt 0)\n        {\n            $owners = ($app.Owners | Where-Object { [string]::IsNullOrEmpty($_.DeletedDateTime) }).Id | ConvertTo-Json -Compress\n        }\n        $createdDate = $null\n        if ($app.CreatedDateTime)\n        {\n            $createdDate = (Get-Date($app.CreatedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $deletedDate = $null\n        if ($app.DeletedDateTime)\n        {\n            $deletedDate = (Get-Date($app.DeletedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $aadObject = New-Object PSObject -Property @{\n            Timestamp = $timestamp\n            TenantGuid = $tenantId\n            Cloud = $cloudEnvironment\n            ObjectId = $app.Id\n            ObjectType = \"Application\"\n            ObjectSubType = \"N/A\"\n            DisplayName = $app.DisplayName\n            SecurityEnabled = \"N/A\"\n            ApplicationId = $app.AppId\n            Keys = (Build-CredObjectWithDates -appObject $app) | ConvertTo-Json -Compress\n            PrincipalNames = (Build-PrincipalNames -appObject $app) | ConvertTo-Json -Compress\n            Owners = $owners\n            CreatedDate = $createdDate\n            DeletedDate = $deletedDate\n        }\n        $aadObjects += $aadObject    \n    }   \n\n    $jsonExportPath = \"$fileDate-$tenantId-aadobjects-apps.json\"\n    $csvExportPath = \"$fileDate-$tenantId-aadobjects-apps.csv\"\n    \n    $aadObjects | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath\n    \"Exported to JSON: $($aadObjects.Count) lines\"\n    $aadObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json\n    \"JSON Import: $($aadObjectsJson.Count) lines\"\n    $aadObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath\n    \"Export to $csvExportPath\"\n    \n    $csvBlobName = $csvExportPath\n    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n    \n    Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force        \n\n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n    \n    Remove-Item -Path $csvExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $csvExportPath from local disk...\"    \n    \n    Remove-Item -Path $jsonExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $jsonExportPath from local disk...\"    \n}\n\nif (\"ServicePrincipal\" -in $aadObjectsTypes)\n{\n    $aadObjects = @()\n\n    \"Getting AAD service principals...\"\n    $spns = Get-MgServicePrincipal -All -ExpandProperty Owners -Property Id,AppId,DeletedDateTime,DisplayName,KeyCredentials,PasswordCredentials,Owners,ServicePrincipalNames,ServicePrincipalType,AccountEnabled,AlternativeNames\n    \"Found $($spns.Count) AAD service principals\"\n    \n    foreach ($spn in $spns)\n    {\n        $owners = $null\n        if ($spn.Owners.Count -gt 0)\n        {\n            $owners = ($spn.Owners | Where-Object { [string]::IsNullOrEmpty($_.DeletedDateTime) }).Id | ConvertTo-Json -Compress\n        }\n        $deletedDate = $null\n        if ($spn.DeletedDateTime)\n        {\n            $deletedDate = (Get-Date($spn.DeletedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $aadObject = New-Object PSObject -Property @{\n            Timestamp = $timestamp\n            TenantGuid = $tenantId\n            Cloud = $cloudEnvironment\n            ObjectId = $spn.Id\n            ObjectType = \"ServicePrincipal\"\n            ObjectSubType = $spn.ServicePrincipalType\n            DisplayName = $spn.DisplayName\n            SecurityEnabled = $spn.AccountEnabled\n            ApplicationId = $spn.AppId\n            Keys = (Build-CredObjectWithDates -appObject $spn) | ConvertTo-Json -Compress\n            PrincipalNames = (Build-PrincipalNames -appObject $spn) | ConvertTo-Json -Compress\n            Owners = $owners\n            DeletedDate = $deletedDate\n        }\n        $aadObjects += $aadObject    \n    }\n\n    $jsonExportPath = \"$fileDate-$tenantId-aadobjects-spns.json\"\n    $csvExportPath = \"$fileDate-$tenantId-aadobjects-spns.csv\"\n    \n    $aadObjects | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath\n    \"Exported to JSON: $($aadObjects.Count) lines\"\n    $aadObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json\n    \"JSON Import: $($aadObjectsJson.Count) lines\"\n    $aadObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath\n    \"Export to $csvExportPath\"\n    \n    $csvBlobName = $csvExportPath\n    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n    \n    Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force        \n\n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n    \n    Remove-Item -Path $csvExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $csvExportPath from local disk...\"    \n    \n    Remove-Item -Path $jsonExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $jsonExportPath from local disk...\"    \n}\n\nif (\"User\" -in $aadObjectsTypes)\n{\n    $aadObjects = @()\n\n    if ([string]::IsNullOrEmpty($userFilter))\n    {\n        \"Getting AAD users...\"\n        $users = Get-MgUser -All -Property Id,AccountEnabled,DisplayName,UserPrincipalName,UserType,CreatedDateTime,DeletedDateTime    \n    }\n    else\n    {\n        \"Getting AAD users with filter $userFilter...\"\n        $users = Get-MgUser -Filter $userFilter -All -Property Id,AccountEnabled,DisplayName,UserPrincipalName,UserType,CreatedDateTime,DeletedDateTime            \n    }\n    \"Found $($users.Count) AAD users\"\n    \n    foreach ($user in $users)\n    {\n        $createdDate = $null\n        if ($user.CreatedDateTime)\n        {\n            $createdDate = (Get-Date($user.CreatedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $deletedDate = $null\n        if ($user.DeletedDateTime)\n        {\n            $deletedDate = (Get-Date($user.DeletedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $aadObject = New-Object PSObject -Property @{\n            Timestamp = $timestamp\n            TenantGuid = $tenantId\n            Cloud = $cloudEnvironment\n            ObjectId = $user.Id\n            ObjectType = \"User\"\n            ObjectSubType = $user.UserType\n            DisplayName = $user.DisplayName\n            SecurityEnabled = $user.AccountEnabled\n            PrincipalNames = $user.UserPrincipalName\n            CreatedDate = $createdDate\n            DeletedDate = $deletedDate\n        }\n        $aadObjects += $aadObject    \n    }\n\n    $jsonExportPath = \"$fileDate-$tenantId-aadobjects-users.json\"\n    $csvExportPath = \"$fileDate-$tenantId-aadobjects-users.csv\"\n    \n    $aadObjects | Export-Csv -NoTypeInformation -Path $csvExportPath\n    \"Export to $csvExportPath\"\n    \n    $csvBlobName = $csvExportPath\n    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n    \n    Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force        \n\n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n    \n    Remove-Item -Path $csvExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $csvExportPath from local disk...\"    \n}\n\nif (\"Group\" -in $aadObjectsTypes)\n{\n    $aadObjects = @()\n\n    if ([string]::IsNullOrEmpty($groupFilter))\n    {\n        \"Getting AAD groups...\"\n        $groups = Get-MgGroup -All -ExpandProperty Members -Property Id,SecurityEnabled,DisplayName,Members,CreatedDateTime,DeletedDateTime,GroupTypes\n    }\n    else\n    {\n        \"Getting AAD groups with filter $groupFilter...\"\n        $groups = Get-MgGroup -Filter $groupFilter -All -ExpandProperty Members -Property Id,SecurityEnabled,DisplayName,Members,CreatedDateTime,DeletedDateTime,GroupTypes\n    }\n    \"Found $($groups.Count) AAD groups\"\n    \n    foreach ($group in $groups)\n    {\n        $groupMembers = $null\n        if ($group.Members.Count -gt 0)\n        {\n            $groupMembers = $group.Members.Id | ConvertTo-Json -Compress\n        }\n        $createdDate = $null\n        if ($group.CreatedDateTime)\n        {\n            $createdDate = (Get-Date($group.CreatedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $deletedDate = $null\n        if ($group.DeletedDateTime)\n        {\n            $deletedDate = (Get-Date($group.DeletedDateTime)).ToUniversalTime().ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n        }\n        $aadObject = New-Object PSObject -Property @{\n            Timestamp = $timestamp\n            TenantGuid = $tenantId\n            Cloud = $cloudEnvironment\n            ObjectId = $group.Id\n            ObjectType = \"Group\"\n            ObjectSubType = $group.GroupTypes | ConvertTo-Json -Compress\n            DisplayName = $group.DisplayName\n            SecurityEnabled = $group.SecurityEnabled\n            PrincipalNames = $groupMembers\n            CreatedDate = $createdDate\n            DeletedDate = $deletedDate\n        }\n        $aadObjects += $aadObject    \n    }\n\n    $jsonExportPath = \"$fileDate-$tenantId-aadobjects-groups.json\"\n    $csvExportPath = \"$fileDate-$tenantId-aadobjects-groups.csv\"\n    \n    $aadObjects | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath\n    \"Exported to JSON: $($aadObjects.Count) lines\"\n    $aadObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json\n    \"JSON Import: $($aadObjectsJson.Count) lines\"\n    $aadObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath\n    \"Export to $csvExportPath\"\n    \n    $csvBlobName = $csvExportPath\n    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n    \n    Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force        \n\n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n    \n    Remove-Item -Path $csvExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $csvExportPath from local disk...\"    \n    \n    Remove-Item -Path $jsonExportPath -Force\n    \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    \"[$now] Removed $jsonExportPath from local disk...\"    \n}\n\n\"DONE!\""
  },
  {
    "path": "runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGAppGatewayContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argappgwexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allAppGWs = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$appGWsTotal = @()\n$resultsSoFar = 0\n\nWrite-Output \"Querying for Application Gateways properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'Microsoft.Network/applicationGateways'\n| extend gatewayIPsCount = array_length(properties.gatewayIPConfigurations)\n| extend frontendIPsCount = array_length(properties.frontendIPConfigurations)\n| extend frontendPortsCount = array_length(properties.frontendPorts)\n| extend backendPoolsCount = array_length(properties.backendAddressPools)\n| extend httpSettingsCount = array_length(properties.backendHttpSettingsCollection)\n| extend httpListenersCount = array_length(properties.httpListeners)\n| extend urlPathMapsCount = array_length(properties.urlPathMaps)\n| extend requestRoutingRulesCount = array_length(properties.requestRoutingRules)\n| extend probesCount = array_length(properties.probes)\n| extend rewriteRulesCount = array_length(properties.rewriteRuleSets)\n| extend redirectConfsCount = array_length(properties.redirectConfigurations)\n| project id, name, resourceGroup, subscriptionId, tenantId, location, zones, skuName = properties.sku.name, skuTier = properties.sku.tier, skuCapacity = properties.sku.capacity, enableHttp2 = properties.enableHttp2, gatewayIPsCount, frontendIPsCount, frontendPortsCount, httpSettingsCount, httpListenersCount, backendPoolsCount, urlPathMapsCount, requestRoutingRulesCount, probesCount, rewriteRulesCount, redirectConfsCount, tags\n| join kind=leftouter (\n\tresources\n\t| where type =~ 'Microsoft.Network/applicationGateways'\n\t| mvexpand backendPools = properties.backendAddressPools\n\t| extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations)\n    | extend backendAddressesCount = array_length(backendPools.properties.backendAddresses)\n\t| summarize backendIPCount = sum(backendIPCount), backendAddressesCount = sum(backendAddressesCount) by id\n) on id\n| project-away id1\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $appGWs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $appGWs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($appGWs -and $appGWs.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $appGWs = $appGWs.Data\n    }\n    $resultsCount = $appGWs.Count\n    $resultsSoFar += $resultsCount\n    $appGWsTotal += $appGWs\n\n} while ($resultsCount -eq $ARGPageSize)\n\nWrite-Output \"Found $($appGWsTotal.Count) Application Gateway entries\"\n\n<#\n    Building CSV entries \n#>\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nforeach ($appGW in $appGWsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $appGW.tenantId\n        SubscriptionGuid = $appGW.subscriptionId\n        ResourceGroupName = $appGW.resourceGroup.ToLower()\n        InstanceName = $appGW.name.ToLower()\n        InstanceId = $appGW.id.ToLower()\n        SkuName = $appGW.skuName\n        SkuTier = $appGW.skuTier\n        SkuCapacity = $appGW.skuCapacity\n        Location = $appGW.location\n        Zones = $appGW.zones\n        EnableHttp2 = $appGW.enableHttp2\n        GatewayIPsCount = $appGW.gatewayIPsCount\n        FrontendIPsCount = $appGW.frontendIPsCount\n        FrontendPortsCount = $appGW.frontendPortsCount\n        BackendIPCount = $appGW.backendIPCount\n        BackendAddressesCount = $appGW.backendAddressesCount\n        HttpSettingsCount = $appGW.httpSettingsCount\n        HttpListenersCount = $appGW.httpListenersCount\n        BackendPoolsCount = $appGW.backendPoolsCount\n        ProbesCount = $appGW.probesCount\n        UrlPathMapsCount = $appGW.urlPathMapsCount\n        RequestRoutingRulesCount = $appGW.requestRoutingRulesCount\n        RewriteRulesCount = $appGW.rewriteRulesCount\n        RedirectConfsCount = $appGW.redirectConfsCount\n        StatusDate = $statusDate\n        Tags = $appGW.tags\n    }\n    \n    $allAppGWs += $logentry\n}\n\n<#\n    Actually exporting CSV to Azure Storage\n#>\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-appgws-$subscriptionSuffix.csv\"\n\n$allAppGWs | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGAppServicePlanContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argappserviceplanexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allasp = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$aspTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for App Service Plan properties\"\n\n$argQuery = @\"\n    resources\n    | where type =~ 'microsoft.web/serverfarms'\n    | extend skuName = sku.name, skuTier = sku.tier, skuCapacity = sku.capacity, skuFamily = sku.family, skuSize = sku.size\n    | extend computeMode = properties.computeMode, zoneRedundant = properties.zoneRedundant\n    | extend numberOfWorkers = properties.numberOfWorkers, currentNumberOfWorkers = properties.currentNumberOfWorkers, maximumNumberOfWorkers = properties.maximumNumberOfWorkers\n    | extend numberOfSites = properties.numberOfSites, planName = properties.planName\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $asp = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $asp = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($asp -and $asp.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $asp = $asp.Data\n    }\n    $resultsCount = $asp.Count\n    $resultsSoFar += $resultsCount\n    $aspTotal += $asp\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($aspTotal.Count) App Service Plan entries\"\n\nforeach ($asplan in $aspTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $asplan.tenantId\n        SubscriptionGuid = $asplan.subscriptionId\n        ResourceGroupName = $asplan.resourceGroup.ToLower()\n        ZoneRedundant = $asplan.zoneRedundant\n        Location = $asplan.location\n        AppServicePlanName = $asplan.name.ToLower()\n        InstanceId = $asplan.id.ToLower()\n        Kind = $asplan.kind\n        SkuName = $asplan.skuName\n        SkuTier = $asplan.skuTier\n        SkuCapacity = $asplan.skuCapacity\n        SkuFamily = $asplan.skuFamily\n        SkuSize = $asplan.skuSize\n        ComputeMode = $asplan.computeMode\n        NumberOfWorkers = $asplan.numberOfWorkers\n        CurrentNumberOfWorkers = $asplan.currentNumberOfWorkers\n        MaximumNumberOfWorkers = $asplan.maximumNumberOfWorkers\n        NumberOfSites = $asplan.numberOfSites\n        PlanName = $asplan.planName\n        Tags = $asplan.tags\n        StatusDate = $statusDate\n    }\n    \n    $allasp += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-asp-$subscriptionSuffix.csv\"\n\n$allasp | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGAvailabilitySetContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argavailsetexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allAvSets = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$avSetsTotal = @()\n$resultsSoFar = 0\n\nWrite-Output \"Querying for Availability Set properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'Microsoft.Compute/availabilitySets'\n| project id, name, location, resourceGroup, subscriptionId, tenantId, skuName = tostring(sku.name), faultDomains = tostring(properties.platformFaultDomainCount), updateDomains = tostring(properties.platformUpdateDomainCount), vmCount = array_length(properties.virtualMachines), tags, zones\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $avSets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $avSets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($avSets -and $avSets.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $avSets = $avSets.Data\n    }\n    $resultsCount = $avSets.Count\n    $resultsSoFar += $resultsCount\n    $avSetsTotal += $avSets\n\n} while ($resultsCount -eq $ARGPageSize)\n\nWrite-Output \"Found $($avSetsTotal.Count) Availability Set entries\"\n\n<#\n    Building CSV entries \n#>\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nforeach ($avSet in $avSetsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $avSet.tenantId\n        SubscriptionGuid = $avSet.subscriptionId\n        ResourceGroupName = $avSet.resourceGroup.ToLower()\n        InstanceName = $avSet.name.ToLower()\n        InstanceId = $avSet.id.ToLower()\n        SkuName = $avSet.skuName\n        Location = $avSet.location\n        FaultDomains = $avSet.faultDomains\n        UpdateDomains = $avSet.updateDomains\n        VmCount = $avSet.vmCount\n        StatusDate = $statusDate\n        Tags = $avSet.tags\n        Zones = $avSet.zones\n    }\n    \n    $allAvSets += $logentry\n}\n\n<#\n    Actually exporting CSV to Azure Storage\n#>\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-availsets-$subscriptionSuffix.csv\"\n\n$allAvSets | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGLoadBalancerContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"arglbexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allLBs = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$LBsTotal = @()\n$resultsSoFar = 0\n\nWrite-Output \"Querying for Load Balancer properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'Microsoft.Network/loadBalancers'\n| extend lbType = iif(properties.frontendIPConfigurations contains 'publicIPAddress', 'Public', iif(properties.frontendIPConfigurations contains 'privateIPAddress', 'Internal', 'Unknown'))\n| extend lbRulesCount = array_length(properties.loadBalancingRules)\n| extend frontendIPsCount = array_length(properties.frontendIPConfigurations)\n| extend inboundNatRulesCount = array_length(properties.inboundNatRules)\n| extend outboundRulesCount = array_length(properties.outboundRules)\n| extend inboundNatPoolsCount = array_length(properties.inboundNatPools)\n| extend backendPoolsCount = array_length(properties.backendAddressPools)\n| extend probesCount = array_length(properties.probes)\n| project id, name, resourceGroup, subscriptionId, tenantId, location, skuName = sku.name, skuTier = sku.tier, lbType, lbRulesCount, frontendIPsCount, inboundNatRulesCount, outboundRulesCount, inboundNatPoolsCount, backendPoolsCount, probesCount, tags\n| join kind=leftouter (\n\tresources\n\t| where type =~ 'Microsoft.Network/loadBalancers'\n\t| mvexpand backendPools = properties.backendAddressPools\n\t| extend backendIPCount = array_length(backendPools.properties.backendIPConfigurations)\n\t| extend backendAddressesCount = array_length(backendPools.properties.loadBalancerBackendAddresses)\n\t| summarize backendIPCount = sum(backendIPCount), backendAddressesCount = sum(backendAddressesCount) by id\n) on id\n| project-away id1\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $LBs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $LBs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($LBs -and $LBs.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $LBs = $LBs.Data\n    }\n    $resultsCount = $LBs.Count\n    $resultsSoFar += $resultsCount\n    $LBsTotal += $LBs\n\n} while ($resultsCount -eq $ARGPageSize)\n\nWrite-Output \"Found $($LBsTotal.Count) Load Balancer entries\"\n\n<#\n    Building CSV entries \n#>\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nforeach ($lb in $LBsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $lb.tenantId\n        SubscriptionGuid = $lb.subscriptionId\n        ResourceGroupName = $lb.resourceGroup.ToLower()\n        InstanceName = $lb.name.ToLower()\n        InstanceId = $lb.id.ToLower()\n        SkuName = $lb.skuName\n        SkuTier = $lb.skuTier\n        Location = $lb.location\n        LbType = $lb.lbType\n        LbRulesCount = $lb.lbRulesCount\n        InboundNatRulesCount = $lb.inboundNatRulesCount\n        OutboundRulesCount = $lb.outboundRulesCount\n        FrontendIPsCount = $lb.frontendIPsCount\n        BackendIPCount = $lb.backendIPCount\n        BackendAddressesCount = $lb.backendAddressesCount\n        InboundNatPoolsCount = $lb.inboundNatPoolsCount\n        BackendPoolsCount = $lb.backendPoolsCount\n        ProbesCount = $lb.probesCount\n        StatusDate = $statusDate\n        Tags = $lb.tags\n    }\n    \n    $allLBs += $logentry\n}\n\n<#\n    Actually exporting CSV to Azure Storage\n#>\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-lbs-$subscriptionSuffix.csv\"\n\n$allLBs | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGDiskContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argdiskexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$alldisks = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$mdisksTotal = @()\n$resultsSoFar = 0\n\n<#\n   Getting all Managed Disks properties with Azure Resource Graph query\n#>\n\nWrite-Output \"Querying for ARM Managed Disks properties\"\n\n$argQuery = @\"\n    resources \n    | where type =~ 'Microsoft.Compute/disks' \n    | extend DiskId = tolower(id), OwnerVmId = tolower(managedBy) \n    | join kind=leftouter (\n        resources \n        | where type =~ 'Microsoft.Compute/virtualMachines' and array_length(properties.storageProfile.dataDisks) > 0 \n        | extend OwnerVmId = tolower(id) \n        | mv-expand DataDisks = properties.storageProfile.dataDisks \n        | extend DiskId = tolower(DataDisks.managedDisk.id), diskCaching = tostring(DataDisks.caching), diskType = 'Data' \n        | project DiskId, OwnerVmId, diskCaching, diskType \n        | union (\n            resources \n            | where type =~ 'Microsoft.Compute/virtualMachines' \n            | extend OwnerVmId = tolower(id) \n            | extend DiskId = tolower(properties.storageProfile.osDisk.managedDisk.id), diskCaching = tostring(properties.storageProfile.osDisk.caching), diskType = 'OS' \n            | project DiskId, OwnerVmId, diskCaching, diskType\n        )\n    ) on OwnerVmId, DiskId \n    | project-away OwnerVmId, DiskId, OwnerVmId1, DiskId1 \n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($mdisks -and $mdisks.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $mdisks = $mdisks.Data\n    }\n    $resultsCount = $mdisks.Count\n    $resultsSoFar += $resultsCount\n    $mdisksTotal += $mdisks\n\n} while ($resultsCount -eq $ARGPageSize)\n\nWrite-Output \"Found $($mdisksTotal.Count) Managed Disk entries\"\n\n<#\n    Building CSV entries \n#>\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nforeach ($disk in $mdisksTotal)\n{\n    $ownerVmId = $null\n    if ($null -ne $disk.managedBy)\n    {\n        $ownerVmId = $disk.managedBy.ToLower()\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $disk.tenantId\n        SubscriptionGuid = $disk.subscriptionId\n        ResourceGroupName = $disk.resourceGroup.ToLower()\n        DiskName = $disk.name.ToLower()\n        InstanceId = $disk.id.ToLower()\n        Location = $disk.location\n        OwnerVMId = $ownerVmId\n        DeploymentModel = \"Managed\"\n        DiskType = $disk.diskType\n        TimeCreated = $disk.properties.timeCreated \n        DiskIOPS = $disk.properties.diskIOPSReadWrite \n        DiskThroughput = $disk.properties.diskMBpsReadWrite\n        DiskTier = $disk.properties.tier\n        DiskState = $disk.properties.diskState\n        EncryptionType = $disk.properties.encryption.type\n        Zones = $disk.zones\n        Caching = $disk.diskCaching \n        DiskSizeGB = $disk.properties.diskSizeGB\n        SKU = $disk.sku.name\n        StatusDate = $statusDate\n        Tags = $disk.tags\n    }\n    \n    $alldisks += $logentry\n}\n\n<#\n    Actually exporting CSV to Azure Storage\n#>\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-disks-$subscriptionSuffix.csv\"\n\n$alldisks | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    \n"
  },
  {
    "path": "runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGNICContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argnicexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allnics = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$nicsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for NIC properties\"\n\n$argQuery = @\"\n    resources\n    | where type =~ 'microsoft.network/networkinterfaces'\n    | extend isPrimary = properties.primary\n    | extend enableAcceleratedNetworking = properties.enableAcceleratedNetworking\n    | extend enableIPForwarding = properties.enableIPForwarding\n    | extend tapConfigurationsCount = array_length(properties.tapConfigurations)\n    | extend hostedWorkloadsCount = array_length(properties.hostedWorkloads)\n    | extend internalDomainNameSuffix = properties.dnsSettings.internalDomainNameSuffix\n    | extend appliedDnsServers = properties.dnsSettings.appliedDnsServers\n    | extend dnsServers = properties.dnsSettings.dnsServers\n    | extend ownerVMId = tolower(properties.virtualMachine.id)\n    | extend ownerPEId = tolower(properties.privateEndpoint.id)\n    | extend macAddress = properties.macAddress\n    | extend nicType = properties.nicType\n    | extend nicNsgId = tolower(properties.networkSecurityGroup.id)\n\t| mv-expand ipconfigs = properties.ipConfigurations\n    | project-away properties\n    | extend privateIPAddressVersion = tostring(ipconfigs.properties.privateIPAddressVersion)\n    | extend privateIPAllocationMethod = tostring(ipconfigs.properties.privateIPAllocationMethod)\n    | extend isIPConfigPrimary = tostring(ipconfigs.properties.primary)\n    | extend privateIPAddress = tostring(ipconfigs.properties.privateIPAddress)\n    | extend publicIPId = tolower(ipconfigs.properties.publicIPAddress.id)\n    | extend IPConfigName = tostring(ipconfigs.name)\n    | extend subnetId = tolower(ipconfigs.properties.subnet.id)\n    | project-away ipconfigs\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $nics = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $nics = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($nics -and $nics.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $nics = $nics.Data\n    }\n    $resultsCount = $nics.Count\n    $resultsSoFar += $resultsCount\n    $nicsTotal += $nics\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($nicsTotal.Count) ARM VNet nic entries\"\n\nforeach ($nic in $nicsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $nic.tenantId\n        SubscriptionGuid = $nic.subscriptionId\n        ResourceGroupName = $nic.resourceGroup.ToLower()\n        Location = $nic.location\n        Name = $nic.name.ToLower()\n        InstanceId = $nic.id.ToLower()\n        IsPrimary = $nic.isPrimary\n        EnableAcceleratedNetworking = $nic.enableAcceleratedNetworking\n        EnableIPForwarding = $nic.enableIPForwarding\n        TapConfigurationsCount = $nic.tapConfigurationsCount\n        HostedWorkloadsCount = $nic.hostedWorkloadsCount\n        InternalDomainNameSuffix = $nic.internalDomainNameSuffix\n        AppliedDnsServers = $nic.appliedDnsServers\n        DnsServers = $nic.dnsServers\n        OwnerVMId = $nic.ownerVMId\n        OwnerPEId = $nic.ownerPEId\n        MacAddress = $nic.macAddress\n        NicType = $nic.nicType\n        NicNSGId = $nic.nicNsgId\n        PrivateIPAddressVersion = $nic.privateIPAddressVersion\n        PrivateIPAllocationMethod = $nic.privateIPAllocationMethod\n        IsIPConfigPrimary = $nic.isIPConfigPrimary\n        PrivateIPAddress = $nic.privateIPAddress\n        PublicIPId = $nic.publicIPId\n        IPConfigName = $nic.IPConfigName\n        SubnetId = $nic.subnetId\n        Tags = $nic.tags\n        StatusDate = $statusDate\n    }\n    \n    $allnics += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-nics-$subscriptionSuffix.csv\"\n\n$allnics | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGNSGContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argnsgexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allnsgRules = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$nsgRulesTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for NSG properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'Microsoft.Network/networkSecurityGroups' \n| extend nicCount = iif(isnotempty(properties.networkInterfaces),array_length(properties.networkInterfaces),0)\n| extend subnetCount = iif(isnotempty(properties.subnets),array_length(properties.subnets),0)\n| mvexpand securityRules = properties.securityRules\n| extend ruleName = tolower(securityRules.name)\n| extend ruleProtocol = tolower(securityRules.properties.protocol)\n| extend ruleDirection = tolower(securityRules.properties.direction)\n| extend rulePriority = toint(securityRules.properties.priority)\n| extend ruleAccess = tolower(securityRules.properties.access)\n| extend ruleDestinationAddresses = tolower(iif(array_length(securityRules.properties.destinationAddressPrefixes) > 0,strcat_array(securityRules.properties.destinationAddressPrefixes, ','),securityRules.properties.destinationAddressPrefix))\n| extend ruleSourceAddresses = tolower(iif(array_length(securityRules.properties.sourceAddressPrefixes) > 0,strcat_array(securityRules.properties.sourceAddressPrefixes, ','),securityRules.properties.sourceAddressPrefix))\n| extend ruleDestinationPorts = iif(array_length(securityRules.properties.destinationPortRanges) > 0,strcat_array(securityRules.properties.destinationPortRanges, ','),securityRules.properties.destinationPortRange)\n| extend ruleSourcePorts = iif(array_length(securityRules.properties.sourcePortRanges) > 0,strcat_array(securityRules.properties.sourcePortRanges, ','),securityRules.properties.sourcePortRange)\n| extend ruleId = tolower(securityRules.id)\n| project-away securityRules, properties\n| order by ruleId asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $nsgRules = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $nsgRules = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($nsgRules -and $nsgRules.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $nsgRules = $nsgRules.Data\n    }\n    $resultsCount = $nsgRules.Count\n    $resultsSoFar += $resultsCount\n    $nsgRulesTotal += $nsgRules\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($nsgRulesTotal.Count) ARM NSG entries\"\n\nforeach ($nsgRule in $nsgRulesTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $nsgRule.tenantId\n        SubscriptionGuid = $nsgRule.subscriptionId\n        ResourceGroupName = $nsgRule.resourceGroup.ToLower()\n        Location = $nsgRule.location\n        NSGName = $nsgRule.name.ToLower()\n        InstanceId = $nsgRule.id.ToLower()\n        NicCount = $nsgRule.nicCount\n        SubnetCount = $nsgRule.subnetCount\n        RuleName = $nsgRule.ruleName\n        RuleProtocol = $nsgRule.ruleProtocol\n        RuleDirection = $nsgRule.ruleDirection\n        RulePriority = $nsgRule.rulePriority\n        RuleAccess = $nsgRule.ruleAccess\n        RuleDestinationAddresses = $nsgRule.ruleDestinationAddresses\n        RuleSourceAddresses = $nsgRule.ruleSourceAddresses\n        RuleDestinationPorts = $nsgRule.ruleDestinationPorts\n        RuleSourcePorts = $nsgRule.ruleSourcePorts\n        Tags = $nsgRule.tags\n        StatusDate = $statusDate\n    }\n    \n    $allnsgRules += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-nsgrules-$subscriptionSuffix.csv\"\n\n$allnsgRules | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGPublicIpContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argpublicipexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allpips = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$pipsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for ARM Public IP properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'microsoft.network/publicipaddresses'\n| extend skuName = tolower(sku.name)\n| extend skuTier = tolower(sku.tier)\n| extend allocationMethod = tolower(properties.publicIPAllocationMethod)\n| extend addressVersion = tolower(properties.publicIPAddressVersion)\n| extend associatedResourceId = iif(isnotempty(properties.ipConfiguration.id),tolower(properties.ipConfiguration.id),tolower(properties.natGateway.id))\n| extend ipAddress = tostring(properties.ipAddress)\n| extend fqdn = tolower(properties.dnsSettings.fqdn)\n| extend publicIpPrefixId = tostring(properties.publicIPPrefix.id)\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($pips -and $pips.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $pips = $pips.Data\n    }\n    $resultsCount = $pips.Count\n    $resultsSoFar += $resultsCount\n    $pipsTotal += $pips\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($pipsTotal.Count) ARM Public IP entries\"\n\nforeach ($pip in $pipsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $pip.tenantId\n        SubscriptionGuid = $pip.subscriptionId\n        ResourceGroupName = $pip.resourceGroup.ToLower()\n        Location = $pip.location\n        Name = $pip.name.ToLower()\n        InstanceId = $pip.id.ToLower()\n        Model = \"ARM\"\n        SkuName = $pip.skuName\n        SkuTier = $pip.skuTier\n        AllocationMethod = $pip.allocationMethod\n        AddressVersion = $pip.addressVersion\n        AssociatedResourceId = $pip.associatedResourceId\n        PublicIpPrefixId = $pip.publicIpPrefixId\n        IPAddress = $pip.ipAddress\n        FQDN = $pip.fqdn\n        Zones = $pip.zones\n        Tags = $pip.tags\n        StatusDate = $statusDate\n    }\n    \n    $allpips += $logentry\n}\n\n$pipsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for Classic Reserved IP properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'microsoft.classicnetwork/reservedips'\n| extend ipAddress = tostring(properties.ipAddress)\n| extend allocationMethod = 'static'\n| extend addressVersion = 'ipv4'\n| extend associatedResourceId = tolower(properties.attachedTo.id)\n| extend ipAddress = tostring(properties.ipAddress)\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $pips = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($pips -and $pips.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $pips = $pips.Data\n    }\n    $resultsCount = $pips.Count\n    $resultsSoFar += $resultsCount\n    $pipsTotal += $pips\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($pipsTotal.Count) Classic Reserved IP entries\"\n\nforeach ($pip in $pipsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $pip.tenantId\n        SubscriptionGuid = $pip.subscriptionId\n        ResourceGroupName = $pip.resourceGroup.ToLower()\n        Location = $pip.location\n        Name = $pip.name.ToLower()\n        InstanceId = $pip.id.ToLower()\n        Model = \"Classic\"\n        AllocationMethod = $pip.allocationMethod\n        AddressVersion = $pip.addressVersion\n        AssociatedResourceId = $pip.associatedResourceId\n        IPAddress = $pip.ipAddress\n        StatusDate = $statusDate\n    }\n    \n    $allpips += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-publicips-$subscriptionSuffix.csv\"\n\n$allpips | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGResourceContainersContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argrescontainersexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allResourceContainers = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$rgsTotal = @()\n$subsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for resource groups...\"\n\n$argQuery = @\"\n    resourcecontainers\n    | where type == \"microsoft.resources/subscriptions/resourcegroups\"\n    | join kind=leftouter (\n        resources\n        | summarize ResourceCount= count() by subscriptionId, resourceGroup\t\n    ) on subscriptionId, resourceGroup\n    | extend ResourceCount = iif(isempty(ResourceCount), 0, ResourceCount)\n    | project id, name, type, tenantId, location, subscriptionId, managedBy, tags, properties, ResourceCount\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $rgs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $rgs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($rgs -and $rgs.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $rgs = $rgs.Data\n    }\n    $resultsCount = $rgs.Count\n    $resultsSoFar += $resultsCount\n    $rgsTotal += $rgs\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for subscriptions\"\n\n$argQuery = @\"\n    resourcecontainers\n    | where type == \"microsoft.resources/subscriptions\"\n    | join kind=leftouter (\n        resources\n        | summarize ResourceCount= count() by subscriptionId\n    ) on subscriptionId\n    | extend ResourceCount = iif(isempty(ResourceCount), 0, ResourceCount)\n    | project id, name, type, tenantId, subscriptionId, managedBy, tags, properties, ResourceCount\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $subs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $subs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($subs -and $subs.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $subs = $subs.Data\n    }\n    $resultsCount = $subs.Count\n    $resultsSoFar += $resultsCount\n    $subsTotal += $subs\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($rgsTotal.Count) RG entries\"\n\nforeach ($rg in $rgsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $rg.tenantId\n        SubscriptionGuid = $rg.subscriptionId\n        Location = $rg.location\n        ContainerType = $rg.type\n        ContainerName = $rg.name.ToLower()\n        InstanceId = $rg.id.ToLower()\n        ResourceCount = $rg.ResourceCount\n        ManagedBy = $rg.managedBy\n        ContainerProperties = $rg.properties | ConvertTo-Json -Compress\n        Tags = $rg.tags\n        StatusDate = $statusDate\n    }\n    \n    $allResourceContainers += $logentry\n}\n\nWrite-Output \"Building $($subsTotal.Count) subscription entries\"\n\nforeach ($sub in $subsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $sub.tenantId\n        SubscriptionGuid = $sub.subscriptionId\n        Location = $sub.location\n        ContainerType = $sub.type\n        ContainerName = $sub.name.ToLower()\n        InstanceId = $sub.id.ToLower()\n        ResourceCount = $sub.ResourceCount\n        ManagedBy = $sub.managedBy\n        ContainerProperties = $sub.properties | ConvertTo-Json -Compress\n        Tags = $sub.tags\n        StatusDate = $statusDate\n    }\n        \n    $allResourceContainers += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$jsonExportPath = \"$today-rescontainers-$subscriptionSuffix.json\"\n$csvExportPath = \"$today-rescontainers-$subscriptionSuffix.csv\"\n\n$allResourceContainers | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath\nWrite-Output \"Exported to JSON: $($allResourceContainers.Count) lines\"\n$allResourceContainersJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json\nWrite-Output \"JSON Import: $($allResourceContainersJson.Count) lines\"\n$allResourceContainersJson | Export-Csv -NoTypeInformation -Path $csvExportPath\nWrite-Output \"Export to $csvExportPath\"\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    \n\nRemove-Item -Path $jsonExportPath -Force\n    \n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGSqlDatabaseContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argsqldbexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$alldbs = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$dbsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for SQL Databases properties\"\n\n$argQuery = @\"\n    resources \n    | where type =~ 'microsoft.sql/servers/databases' and name != 'master'\n    | extend skuName = sku.name, skuTier = sku.tier, skuCapacity = sku.capacity\n    | extend storageAccountType = properties.storageAccountType, licenseType = properties.licenseType, serviceObjectiveName = properties.currentServiceObjectiveName\n    | extend zoneRedundant = properties.zoneRedundant, maxSizeBytes = properties.maxSizeBytes, maxLogSizeBytes = properties.maxLogSizeBytes\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $dbs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $dbs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($dbs -and $dbs.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $dbs = $dbs.Data\n    }\n    $resultsCount = $dbs.Count\n    $resultsSoFar += $resultsCount\n    $dbsTotal += $dbs\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($dbsTotal.Count) SQL Database entries\"\n\nforeach ($db in $dbsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $db.tenantId\n        SubscriptionGuid = $db.subscriptionId\n        ResourceGroupName = $db.resourceGroup.ToLower()\n        ZoneRedundant = $db.zoneRedundant\n        Location = $db.location\n        DBName = $db.name.ToLower()\n        InstanceId = $db.id.ToLower()\n        SkuName = $db.skuName\n        SkuTier = $db.skuTier\n        SkuCapacity = $db.skuCapacity\n        ServiceObjectiveName = $db.serviceObjectiveName\n        StorageAccountType = $db.storageAccountType\n        LicenseType = $db.licenseType\n        MaxSizeBytes = $db.maxSizeBytes\n        MaxLogSizeBytes = $db.maxLogSizeBytes\n        Tags = $db.tags\n        StatusDate = $statusDate\n    }\n    \n    $alldbs += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-sqldbs-$subscriptionSuffix.csv\"\n\n$alldbs | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGVhdContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argvhdexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$alldisks = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$mdisksTotal = @()\n$resultsSoFar = 0\n\nWrite-Output \"Querying for ARM Unmanaged OS Disks properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'Microsoft.Compute/virtualMachines' and isnull(properties.storageProfile.osDisk.managedDisk)\n| extend diskType = 'OS', diskCaching = tostring(properties.storageProfile.osDisk.caching), diskSize = tostring(properties.storageProfile.osDisk.diskSizeGB)\n| extend vhdUriParts = split(tostring(properties.storageProfile.osDisk.vhd.uri),'/')\n| extend diskStorageAccountName = tostring(split(vhdUriParts[2],'.')[0]), diskContainerName = tostring(vhdUriParts[3]), diskVhdName = tostring(vhdUriParts[4])\n| order by id, diskStorageAccountName, diskContainerName, diskVhdName\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($mdisks -and $mdisks.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $mdisks = $mdisks.Data\n    }\n    $resultsCount = $mdisks.Count\n    $resultsSoFar += $resultsCount\n    $mdisksTotal += $mdisks\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$resultsSoFar = 0\n\nWrite-Output \"Found $($mdisksTotal.Count) Unmanaged OS Disk entries\"\n\nWrite-Output \"Querying for ARM Unmanaged Data Disks properties\"\n\n$argQuery = @\"\nresources\n| where type =~ 'Microsoft.Compute/virtualMachines' and isnull(properties.storageProfile.osDisk.managedDisk)\n| mvexpand dataDisks = properties.storageProfile.dataDisks\n| extend diskType = 'Data', diskCaching = tostring(dataDisks.caching), diskSize = tostring(dataDisks.diskSizeGB)\n| extend vhdUriParts = split(tostring(dataDisks.vhd.uri),'/')\n| extend diskStorageAccountName = tostring(split(vhdUriParts[2],'.')[0]), diskContainerName = tostring(vhdUriParts[3]), diskVhdName = tostring(vhdUriParts[4])\n| order by id, diskStorageAccountName, diskContainerName, diskVhdName\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $mdisks = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($mdisks -and $mdisks.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $mdisks = $mdisks.Data\n    }\n    $resultsCount = $mdisks.Count\n    $resultsSoFar += $resultsCount\n    $mdisksTotal += $mdisks\n\n} while ($resultsCount -eq $ARGPageSize)\n\nWrite-Output \"Found overall $($mdisksTotal.Count) Unmanaged Disk entries\"\n\n<#\n    Building CSV entries \n#>\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nforeach ($disk in $mdisksTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $disk.tenantId\n        SubscriptionGuid = $disk.subscriptionId\n        ResourceGroupName = $disk.resourceGroup.ToLower()\n        DiskName = $disk.diskVhdName.ToLower()\n        InstanceId = ($disk.diskStorageAccountName + \"/\" + $disk.diskContainerName + \"/\" + $disk.diskVhdName).ToLower()\n        OwnerVMId = $disk.id.ToLower()\n        Location = $disk.location\n        DeploymentModel = \"Unmanaged\"\n        DiskType = $disk.diskType \n        Caching = $disk.diskCaching \n        DiskSizeGB = $disk.diskSize\n        StatusDate = $statusDate\n        Tags = $disk.tags\n    }\n    \n    $alldisks += $logentry\n}\n\n<#\n    Actually exporting CSV to Azure Storage\n#>\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-vhds-$subscriptionSuffix.csv\"\n\n$alldisks | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGVMSSContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argvmssexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nWrite-Output \"Getting VM sizes details for $referenceRegion\"\n$sizes = Get-AzVMSize -Location $referenceRegion\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allvmss = @()\n\nif ($TargetSubscription)\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = \"-\" + $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$armVmssTotal = @()\n\n$resultsSoFar = 0\n\n$argQuery = @\"\nresources\n| where type =~ 'microsoft.compute/virtualmachinescalesets'\n| project id, tenantId, name, location, resourceGroup, subscriptionId, skUName = tostring(sku.name),\n    computerNamePrefix = tostring(properties.virtualMachineProfile.osProfile.computerNamePrefix),\n    usesManagedDisks = iif(isnull(properties.virtualMachineProfile.storageProfile.osDisk.managedDisk), 'false', 'true'),\n\tcapacity = tostring(sku.capacity), priority = tostring(properties.virtualMachineProfile.priority), tags, zones,\n\tosType = iif(isnotnull(properties.virtualMachineProfile.osProfile.linuxConfiguration), \"Linux\", \"Windows\"),\n\tosDiskSize = tostring(properties.virtualMachineProfile.storageProfile.osDisk.diskSizeGB),\n\tosDiskCaching = tostring(properties.virtualMachineProfile.storageProfile.osDisk.caching),\n\tosDiskSKU = tostring(properties.virtualMachineProfile.storageProfile.osDisk.managedDisk.storageAccountType),\n\tdataDiskCount = iif(isnotnull(properties.virtualMachineProfile.storageProfile.dataDisks), array_length(properties.virtualMachineProfile.storageProfile.dataDisks), 0),\n\tnicCount = array_length(properties.virtualMachineProfile.networkProfile.networkInterfaceConfigurations),\n    imagePublisher = iif(isnotempty(properties.virtualMachineProfile.storageProfile.imageReference.publisher),tostring(properties.virtualMachineProfile.storageProfile.imageReference.publisher),'Custom'),\n    imageOffer = iif(isnotempty(properties.virtualMachineProfile.storageProfile.imageReference.offer),tostring(properties.virtualMachineProfile.storageProfile.imageReference.offer),tostring(properties.virtualMachineProfile.storageProfile.imageReference.id)),\n    imageSku = tostring(properties.virtualMachineProfile.storageProfile.imageReference.sku),\n    imageVersion = tostring(properties.virtualMachineProfile.storageProfile.imageReference.version),\n    imageExactVersion = tostring(properties.virtualMachineProfile.storageProfile.imageReference.exactVersion),\n\tsinglePlacementGroup = tostring(properties.singlePlacementGroup),\n\tupgradePolicy = tostring(properties.upgradePolicy.mode),\n\toverProvision = tostring(properties.overprovision),\n\tplatformFaultDomainCount = tostring(properties.platformFaultDomainCount),\n    zoneBalance = tostring(properties.zoneBalance)\t\t\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $armVmss = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $armVmss = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions \n    }\n\n    if ($armVmss -and $armVmss.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $armVmss = $armVmss.Data\n    }\n    $resultsCount = $armVmss.Count\n    $resultsSoFar += $resultsCount\n    $armVmssTotal += $armVmss\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($armVmssTotal.Count) VMSS entries\"\n\nforeach ($vmss in $armVmssTotal)\n{\n    $vmSize = $sizes | Where-Object {$_.name -eq $vmss.skUName}\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $vmss.tenantId\n        SubscriptionGuid = $vmss.subscriptionId\n        ResourceGroupName = $vmss.resourceGroup.ToLower()\n        Zones = $vmss.zones\n        Location = $vmss.location\n        VMSSName = $vmss.name.ToLower()\n        ComputerNamePrefix = $vmss.computerNamePrefix.ToLower()\n        InstanceId = $vmss.id.ToLower()\n        VMSSSize = $vmSize.name.ToLower()\n        CoresCount = $vmSize.NumberOfCores\n        MemoryMB = $vmSize.MemoryInMB\n        OSType = $vmss.osType\n        DataDiskCount = $vmss.dataDiskCount\n        NicCount = $vmss.nicCount\n        StatusDate = $statusDate\n        Tags = $vmss.tags\n        Capacity = $vmss.capacity\n        Priority = $vmss.priority\n        OSDiskSize = $vmss.osDiskSize\n        OSDiskCaching = $vmss.osDiskCaching\n        OSDiskSKU = $vmss.osDiskSKU\n        SinglePlacementGroup = $vmss.singlePlacementGroup\n        UpgradePolicy = $vmss.upgradePolicy\n        OverProvision = $vmss.overProvision\n        PlatformFaultDomainCount = $vmss.platformFaultDomainCount\n        ZoneBalance = $vmss.zoneBalance\n        UsesManagedDisks = $vmss.usesManagedDisks\n        ImagePublisher = $vmss.imagePublisher\n        ImageOffer = $vmss.imageOffer\n        ImageSku = $vmss.imageSku\n        ImageVersion = $vmss.imageVersion\n        ImageExactVersion = $vmss.imageExactVersion\n    }\n    \n    $allvmss += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-vmss-$subscriptionSuffix.csv\"\n\n$allvmss | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGVNetContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argvnetexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allsubnets = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$subnetsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for ARM VNet properties\"\n\n$argQuery = @\"\n    resources\n    | where type =~ 'microsoft.network/virtualnetworks'\n    | mv-expand subnets = properties.subnets limit 400\n    | extend peeringsCount = array_length(properties.virtualNetworkPeerings)\n    | extend vnetPrefixes = properties.addressSpace.addressPrefixes\n    | extend dnsServers = properties.dhcpOptions.dnsServers\n    | extend enableDdosProtection = properties.enableDdosProtection\n    | project-away properties\n    | extend subnetPrefix = tostring(subnets.properties.addressPrefix)\n    | extend subnetDelegationsCount = array_length(subnets.properties.delegations)\n    | extend subnetUsedIPs = iif(isnotempty(subnets.properties.ipConfigurations), array_length(subnets.properties.ipConfigurations), 0)\n    | extend subnetTotalPrefixIPs = pow(2, 32 - toint(split(subnetPrefix,'/')[1])) - 5\n    | extend subnetNsgId = tolower(subnets.properties.networkSecurityGroup.id)\n    | project id, vnetName = name, resourceGroup, subscriptionId, tenantId, location, vnetPrefixes, dnsServers, subnetName = tolower(tostring(subnets.name)), subnetPrefix, subnetDelegationsCount, subnetTotalPrefixIPs, subnetUsedIPs, subnetNsgId, peeringsCount, enableDdosProtection, tags\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($subnets -and $subnets.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $subnets = $subnets.Data\n    }\n    $resultsCount = $subnets.Count\n    $resultsSoFar += $resultsCount\n    $subnetsTotal += $subnets\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($subnetsTotal.Count) ARM VNet subnet entries\"\n\nforeach ($subnet in $subnetsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $subnet.tenantId\n        SubscriptionGuid = $subnet.subscriptionId\n        ResourceGroupName = $subnet.resourceGroup.ToLower()\n        Location = $subnet.location\n        VNetName = $subnet.vnetName.ToLower()\n        InstanceId = $subnet.id.ToLower()\n        Model = \"ARM\"\n        VNetPrefixes = $subnet.vnetPrefixes\n        DNSServers = $subnet.dnsServers\n        PeeringsCount = $subnet.peeringsCount\n        EnableDdosProtection = $subnet.enableDdosProtection\n        SubnetName = $subnet.subnetName\n        SubnetPrefix = $subnet.subnetPrefix\n        SubnetDelegationsCount = $subnet.subnetDelegationsCount\n        SubnetTotalPrefixIPs = $subnet.subnetTotalPrefixIPs\n        SubnetUsedIPs = $subnet.subnetUsedIPs\n        SubnetNSGId = $subnet.subnetNsgId\n        Tags = $subnet.tags\n        StatusDate = $statusDate\n    }\n    \n    $allsubnets += $logentry\n}\n\n$subnetsTotal = @()\n\n$resultsSoFar = 0\n\nWrite-Output \"Querying for Classic VNet properties\"\n\n$argQuery = @\"\n    resources\n    | where type =~ 'microsoft.classicnetwork/virtualnetworks'\n    | extend vNetId = tolower(id)\n    | mv-expand subnets = properties.subnets limit 400\n    | extend subnetName = tolower(tostring(subnets.name))\n    | join kind=leftouter (\n        resources\n        | where type =~ 'microsoft.network/virtualnetworks'\n        | mvexpand peerings = properties.virtualNetworkPeerings limit 400\n        | extend vNetId = tolower(tostring(peerings.properties.remoteVirtualNetwork.id))\n        | where vNetId has \"microsoft.classicnetwork\"\n        | summarize vNetPeerings=count() by vNetId\n    ) on vNetId\n    | extend peeringsCount = iif(isnotempty(vNetPeerings), vNetPeerings, 0)\n    | extend vnetPrefixes = properties.addressSpace.addressPrefixes\n    | extend dnsServers = properties.dhcpOptions.dnsServers\n    | project-away properties\n    | extend subnetPrefix = tostring(subnets.addressPrefix)\n    | join kind=leftouter (\n        resources\n        | where type =~ 'microsoft.classiccompute/virtualmachines'\n        | extend networkProfile = properties.networkProfile\n        | mvexpand subnets = networkProfile.virtualNetwork.subnetNames limit 400\n        | extend subnetName = tolower(tostring(subnets))\n        | project id, vNetId = tolower(tostring(networkProfile.virtualNetwork.id)), subnetName\n        | summarize subnetUsedIPs = count() by vNetId, subnetName\n    ) on vNetId and subnetName\n    | extend subnetUsedIPs = iif(isnotempty(subnetUsedIPs), subnetUsedIPs, 0)\n    | extend subnetTotalPrefixIPs = pow(2, 32 - toint(split(subnetPrefix,'/')[1])) - 5\n    | extend enableDdosProtection = 'false'\n    | project vNetId, vnetName = name, resourceGroup, subscriptionId, tenantId, location, vnetPrefixes, dnsServers, subnetName, subnetPrefix, subnetTotalPrefixIPs, subnetUsedIPs, peeringsCount, enableDdosProtection\n    | order by vNetId asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $subnets = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($subnets -and $subnets.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $subnets = $subnets.Data\n    }\n    $resultsCount = $subnets.Count\n    $resultsSoFar += $resultsCount\n    $subnetsTotal += $subnets\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($subnetsTotal.Count) Classic VNet subnet entries\"\n\nforeach ($subnet in $subnetsTotal)\n{\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $subnet.tenantId\n        SubscriptionGuid = $subnet.subscriptionId\n        ResourceGroupName = $subnet.resourceGroup.ToLower()\n        Location = $subnet.location\n        VNetName = $subnet.vnetName.ToLower()\n        InstanceId = $subnet.vNetId.ToLower()\n        Model = \"Classic\"\n        VNetPrefixes = $subnet.vnetPrefixes\n        DNSServers = $subnet.dnsServers\n        PeeringsCount = $subnet.peeringsCount\n        EnableDdosProtection = $subnet.enableDdosProtection\n        SubnetName = $subnet.subnetName\n        SubnetPrefix = $subnet.subnetPrefix\n        SubnetTotalPrefixIPs = $subnet.subnetTotalPrefixIPs\n        SubnetUsedIPs = $subnet.subnetUsedIPs\n        StatusDate = $statusDate\n    }\n    \n    $allsubnets += $logentry\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-vnetsubnets-$subscriptionSuffix.csv\"\n\n$allsubnets | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ARGVMContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"argvmexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n# get list of all VM sizes\nWrite-Output \"Getting VM sizes details for $referenceRegion\"\n$sizes = Get-AzVMSize -Location $referenceRegion\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allvms = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n$armVmsTotal = @()\n$classicVmsTotal = @()\n\n$resultsSoFar = 0\n\n<#\n   Getting all ARM VMs properties with Azure Resource Graph query\n#>\n\nWrite-Output \"Querying for ARM VM properties\"\n\n$argQuery = @\"\n    resources\n    | where type =~ 'Microsoft.Compute/virtualMachines' \n    | extend dataDiskCount = array_length(properties.storageProfile.dataDisks), nicCount = array_length(properties.networkProfile.networkInterfaces) \n    | extend usesManagedDisks = iif(isnull(properties.storageProfile.osDisk.managedDisk), 'false', 'true')\n    | extend availabilitySetId = tostring(properties.availabilitySet.id)\n    | extend bootDiagnosticsEnabled = tostring(properties.diagnosticsProfile.bootDiagnostics.enabled)\n    | extend bootDiagnosticsStorageAccount = split(split(properties.diagnosticsProfile.bootDiagnostics.storageUri, '/')[2],'.')[0]\n    | extend powerState = tostring(properties.extended.instanceView.powerState.code) \n    | extend imagePublisher = iif(isnotempty(properties.storageProfile.imageReference.publisher),tostring(properties.storageProfile.imageReference.publisher),'Custom')\n    | extend imageOffer = iif(isnotempty(properties.storageProfile.imageReference.offer),tostring(properties.storageProfile.imageReference.offer),tostring(properties.storageProfile.imageReference.id))\n    | extend imageSku = tostring(properties.storageProfile.imageReference.sku)\n    | extend imageVersion = tostring(properties.storageProfile.imageReference.version)\n    | extend imageExactVersion = tostring(properties.storageProfile.imageReference.exactVersion)\n    | extend osName = tostring(properties.extended.instanceView.osName)\n    | extend osVersion = tostring(properties.extended.instanceView.osVersion)\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $armVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $armVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($armVms -and $armVms.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $armVms = $armVms.Data\n    }\n    $resultsCount = $armVms.Count\n    $resultsSoFar += $resultsCount\n    $armVmsTotal += $armVms\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$resultsSoFar = 0\n\n<#\n   Getting all Classic VMs properties with Azure Resource Graph query\n#>\n\nWrite-Output \"Querying for Classic VM properties\"\n\n$argQuery = @\"\n    resources\n    | where type =~ 'Microsoft.ClassicCompute/virtualMachines' \n    | extend dataDiskCount = iif(isnotnull(properties.storageProfile.dataDisks), array_length(properties.storageProfile.dataDisks), 0), nicCount = iif(isnotnull(properties.networkProfile.virtualNetwork.networkInterfaces), array_length(properties.networkProfile.virtualNetwork.networkInterfaces) + 1, 1) \n\t| extend usesManagedDisks = 'false'\n\t| extend availabilitySetId = tostring(properties.hardwareProfile.availabilitySet)\n\t| extend bootDiagnosticsEnabled = tostring(properties.debugProfile.bootDiagnosticsEnabled)\n    | extend bootDiagnosticsStorageAccount = split(split(properties.debugProfile.serialOutputBlobUri, '/')[2],'.')[0]\n    | extend powerState = tostring(properties.instanceView.status)\n    | extend imageOffer = tostring(properties.storageProfile.operatingSystemDisk.sourceImageName)\n    | order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $classicVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $classicVms = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($classicVms -and $classicVms.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $classicVms = $classicVms.Data\n    }\n    $resultsCount = $classicVms.Count\n    $resultsSoFar += $resultsCount\n    $classicVmsTotal += $classicVms\n\n} while ($resultsCount -eq $ARGPageSize)\n\n<#\n    Merging ARM + Classic VMs, enriching VM size details and building CSV entries \n#>\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nWrite-Output \"Building $($armVmsTotal.Count) ARM VM entries\"\n\nforeach ($vm in $armVmsTotal)\n{\n    $vmSize = $sizes | Where-Object {$_.name -eq $vm.properties.hardwareProfile.vmSize}\n\n    $avSetId = $null\n    if ($vm.availabilitySetId)\n    {\n        $avSetId = $vm.availabilitySetId.ToLower()\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $vm.tenantId\n        SubscriptionGuid = $vm.subscriptionId\n        ResourceGroupName = $vm.resourceGroup.ToLower()\n        Zones = $vm.zones\n        Location = $vm.location\n        VMName = $vm.name.ToLower()\n        DeploymentModel = 'ARM'\n        InstanceId = $vm.id.ToLower()\n        VMSize = $vm.properties.hardwareProfile.vmSize\n        CoresCount = $vmSize.NumberOfCores\n        MemoryMB = $vmSize.MemoryInMB\n        OSType = $vm.properties.storageProfile.osDisk.osType\n        LicenseType = $vm.properties.licenseType\n        DataDiskCount = $vm.dataDiskCount\n        NicCount = $vm.nicCount\n        UsesManagedDisks = $vm.usesManagedDisks\n        AvailabilitySetId = $avSetId\n        BootDiagnosticsEnabled = $vm.bootDiagnosticsEnabled\n        BootDiagnosticsStorageAccount = $vm.bootDiagnosticsStorageAccount\n        StatusDate = $statusDate\n        PowerState = $vm.powerState\n        ImagePublisher = $vm.imagePublisher\n        ImageOffer = $vm.imageOffer\n        ImageSku = $vm.imageSku\n        ImageVersion = $vm.imageVersion\n        ImageExactVersion = $vm.imageExactVersion\n        OSName = $vm.osName\n        OSVersion = $vm.osVersion\n        Tags = $vm.tags\n    }\n    \n    $allvms += $logentry\n}\n\nWrite-Output \"Building $($classicVmsTotal.Count) Classic VM entries\"\n\nforeach ($vm in $classicVmsTotal)\n{\n    $vmSize = $sizes | Where-Object {$_.name -eq $vm.properties.hardwareProfile.size}\n\n    $avSetId = $null\n    if ($vm.availabilitySetId)\n    {\n        $avSetId = $vm.availabilitySetId.ToLower()\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $vm.tenantId\n        SubscriptionGuid = $vm.subscriptionId\n        ResourceGroupName = $vm.resourceGroup.ToLower()\n        VMName = $vm.name.ToLower()\n        DeploymentModel = 'Classic'\n        Location = $vm.location\n        InstanceId = $vm.id.ToLower()\n        VMSize = $vm.properties.hardwareProfile.size\n        CoresCount = $vmSize.NumberOfCores\n        MemoryMB = $vmSize.MemoryInMB\n        OSType = $vm.properties.storageProfile.operatingSystemDisk.operatingSystem\n        LicenseType = \"N/A\"\n        DataDiskCount = $vm.dataDiskCount\n        NicCount = $vm.nicCount\n        UsesManagedDisks = $vm.usesManagedDisks\n        AvailabilitySetId = $avSetId\n        BootDiagnosticsEnabled = $vm.bootDiagnosticsEnabled\n        BootDiagnosticsStorageAccount = $vm.bootDiagnosticsStorageAccount\n        PowerState = $vm.powerState\n        StatusDate = $statusDate\n        ImagePublisher = $vm.imagePublisher\n        ImageOffer = $vm.imageOffer\n        ImageSku = $vm.imageSku\n        ImageVersion = $vm.imageVersion\n        ImageExactVersion = $vm.imageExactVersion\n        OSName = $vm.osName\n        OSVersion = $vm.osVersion\n        Tags = $null\n    }\n    \n    $allvms += $logentry\n}\n\n<#\n    Actually exporting CSV to Azure Storage\n#>\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-vms-$subscriptionSuffix.csv\"\n\n$allvms | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $targetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_AdvisorContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"advisorexports\"\n}\n\n$CategoryFilter = Get-AutomationVariable -Name  \"AzureOptimization_AdvisorFilter\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($CategoryFilter))\n{\n    $CategoryFilter = \"HighAvailability,Security,Performance,OperationalExcellence\" # comma-separated list of categories\n}\n$CategoryFilter += \",Cost\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$ARGPageSize = 1000\n\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $scope = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" -and $_.SubscriptionPolicies.QuotaId -notlike \"AAD*\" } | ForEach-Object { \"$($_.Id)\"}\n    $scope = $tenantId\n}\n\n\n<#\n   Getting Advisor recommendations for each subscription and building CSV entries\n#>\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$recommendationsARG = @()\n\n$resultsSoFar = 0\n\n$FinalCategoryFilter = \"\"\n\nif (-not([string]::IsNullOrEmpty($CategoryFilter)))\n{\n    $categories = $CategoryFilter.Split(',')\n    for ($i = 0; $i -lt $categories.Count; $i++)\n    {\n        $categories[$i] = \"'\" + $categories[$i] + \"'\"\n    }    \n    $FinalCategoryFilter = \" and properties.category in (\" + ($categories -join \",\") + \")\"\n}\n\n$argQuery = @\"\nadvisorresources\n| where type == 'microsoft.advisor/recommendations'\n| where isnull(properties.suppressionIds)$FinalCategoryFilter\n| extend resourceId = tostring(split(tolower(id),'/providers/microsoft.advisor')[0])\n| join kind=leftouter (resources | project resourceId=tolower(id), resourceTags=tags) on resourceId\n| project id, category = properties.category, impact = properties.impact, impactedArea = properties.impactedField,\n    description = properties.shortDescription.problem, recommendationText = properties.shortDescription.solution,\n    recommendationTypeId = properties.recommendationTypeId, instanceName = properties.impactedValue,\n    additionalInfo = properties.extendedProperties, tags=resourceTags\n| order by id asc\n\"@\n\ndo\n{\n    if ($resultsSoFar -eq 0)\n    {\n        $recs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else\n    {\n        $recs = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($recs -and $recs.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $recs = $recs.Data\n    }\n    $resultsCount = $recs.Count\n    $resultsSoFar += $resultsCount\n    $recommendationsARG += $recs\n\n} while ($resultsCount -eq $ARGPageSize)\n\nWrite-Output \"Building $($recommendationsARG.Count) recommendations entries\"\n\n$recommendations = @()\n\nforeach ($advisorRecommendation in $recommendationsARG)\n{\n    $resourceIdParts = $advisorRecommendation.id.Split('/')\n    if ($resourceIdParts.Count -ge 9)\n    {\n        # if the Resource ID is made of 9 parts, then the recommendation is relative to a specific Azure resource\n        $realResourceIdParts = $resourceIdParts[0..8]\n        $instanceId = ($realResourceIdParts -join \"/\").ToLower()\n        $resourceGroup = $realResourceIdParts[4].ToLower()\n        $subscriptionId = $realResourceIdParts[2]\n    }\n    else\n    {\n        # otherwise it is not a resource-specific recommendation (e.g., reservations)\n        $resourceGroup = \"notavailable\"\n        $instanceId = $advisorRecommendation.id.ToLower()\n        $subscriptionId = $resourceIdParts[2]\n    }\n\n    if (-not([string]::IsNullOrEmpty($advisorRecommendation.additionalInfo)))\n    {\n        $additionalInfo = $advisorRecommendation.additionalInfo | ConvertTo-Json -Compress\n    }\n    else\n    {\n        $additionalInfo = $null\n    }\n    \n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        Category = $advisorRecommendation.category\n        Impact = $advisorRecommendation.impact\n        ImpactedArea = $advisorRecommendation.impactedArea\n        Description = $advisorRecommendation.description\n        RecommendationText = $advisorRecommendation.recommendationText\n        RecommendationTypeId = $advisorRecommendation.recommendationTypeId\n        InstanceId = $instanceId\n        InstanceName = $advisorRecommendation.instanceName\n        Tags = $advisorRecommendation.tags\n        AdditionalInfo = $additionalInfo\n        ResourceGroup = $resourceGroup\n        SubscriptionGuid = $subscriptionId\n        TenantGuid = $tenantId\n    }\n\n    $recommendations += $recommendation    \n}\n\nWrite-Output \"Found $($recommendations.Count) ($CategoryFilter) recommendations...\"\n\n$fileDate = $datetime.ToString(\"yyyyMMdd\")\n$advisorFilter = $CategoryFilter.Replace(',','').ToLower()\n$csvExportPath = \"$fileDate-$advisorFilter-$scope.csv\"\n\n$recommendations | Export-Csv -NoTypeInformation -Path $csvExportPath\nWrite-Output \"Export to $csvExportPath\"\n\n$csvBlobName = $csvExportPath\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    \n\nWrite-Output \"DONE!\""
  },
  {
    "path": "runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1",
    "content": "Param (\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $true)]\n    [string] $ResourceType, # ARM resource type\n\n    [Parameter(Mandatory = $false)]\n    [string] $ARGFilter, # e.g., name != 'master' and sku.tier in ('Basic','Standard','Premium')\n\n    [Parameter(Mandatory = $true)]\n    [string] $MetricNames, # comma-separated metrics names (use Get-AzMetricDefinition for a list of supported metric names for a given resource)\n\n    [Parameter(Mandatory = $true)]\n    [ValidateSet(\"Maximum\", \"Minimum\", \"Average\", \"Total\")]\n    [string] $AggregationType,\n\n    [Parameter(Mandatory = $false)]\n    [ValidateSet(\"Default\", \"Maximum\", \"Minimum\", \"Average\", \"Total\")]\n    [string] $AggregationOfType = \"Default\",\n\n    [Parameter(Mandatory = $true)]\n    [string] $TimeSpan, # [d.]hh:mm:ss\n\n    [Parameter(Mandatory = $true)]\n    [string] $TimeGrain, # [d.]hh:mm:ss (00:01:00, 00:05:00, 00:15:00, 00:30:00, 01:00:00, 06:00:00, 12:00:00, 1.00:00:00, 7.00:00:00, 30.00:00:00)\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_AzMonitorContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"azmonitorexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\nif (-not([string]::IsNullOrEmpty($TargetSubscription))) {\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = \"-\" + $TargetSubscription\n}\nelse {\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\n[TimeSpan]::Parse($TimeGrain) | Out-Null\n$TimeSpanObj = [TimeSpan]::Parse(\"-$TimeSpan\")\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Querying for $ResourceType with page size $ARGPageSize and target subscription $TargetSubscription...\"\n\n$allResources = @()\n\n$resultsSoFar = 0\n\n$argWhere = \"\"\nif (-not([string]::IsNullOrEmpty($ARGFilter)))\n{\n    $argWhere = \" and $ARGFilter\"\n}\n\n$argQuery = @\"\nresources \n| where type =~ '$ResourceType'$argWhere\n| project id, name, subscriptionId, resourceGroup, tenantId \n| order by id asc\n\"@\n\ndo {\n    if ($resultsSoFar -eq 0) {\n        $resources = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n    }\n    else {\n        $resources = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n    }\n    if ($resources -and $resources.GetType().Name -eq \"PSResourceGraphResponse\")\n    {\n        $resources = $resources.Data\n    }\n    $resultsCount = $resources.Count\n    $resultsSoFar += $resultsCount\n    $allResources += $resources\n\n} while ($resultsCount -eq $ARGPageSize)\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Found $($allResources.Count) resources.\"\n\n$metrics = $MetricNames.Split(',')\n\n$queryDate = Get-Date\n$utcNow = $queryDate.ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n$utcAgo = $queryDate.Add($TimeSpanObj).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\n$customMetrics = @()\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Analyzing resources for $MetricNames metrics ($AggregationType with $TimeGrain time grain) since $utcAgo...\"\n\nforeach ($resource in $allResources) {\n    $valuesAggregation = @()\n    $foundResource = $true\n    foreach ($metric in $metrics) {\n        $metricValues = Get-AzMetric -ResourceId $resource.id -MetricName $metric -TimeGrain $TimeGrain -AggregationType $AggregationType `\n            -StartTime $utcAgo -EndTime $utcNow -WarningAction SilentlyContinue -ErrorAction Continue\n        if ($metricValues.Data) {\n            if ($valuesAggregation.Count -eq 0) {\n                $valuesAggregation = $metricValues.Data.\"$AggregationType\"\n            }\n            else {\n                for ($i = 0; $i -lt $valuesAggregation.Count; $i++) {\n                    if ($metricValues.Data.Count -gt 1)\n                    {\n                        $valuesAggregation[$i] += $metricValues.Data[$i].\"$AggregationType\"\n                    }\n                    else\n                    {\n                        $valuesAggregation += $metricValues.Data.\"$AggregationType\"\n                    }\n                }\n            }    \n        }\n        \n        if (-not($metricValues.Id))\n        {\n            $foundResource = $false    \n        }\n    }\n\n    if ($foundResource)\n    {\n        $aggregatedValue = $null\n        $finalAggregationType = $AggregationType\n        if ($AggregationOfType -ne \"Default\")\n        {\n            $finalAggregationType = $AggregationOfType\n        }\n        if ($valuesAggregation.Count -gt 0) {\n            switch ($finalAggregationType) {\n                \"Maximum\" {\n                    $aggregatedValue = ($valuesAggregation | Measure-Object -Maximum).Maximum\n                }\n                \"Minimum\" {\n                    $aggregatedValue = ($valuesAggregation | Measure-Object -Minimum).Minimum\n                }\n                \"Average\" {\n                    $aggregatedValue = ($valuesAggregation | Measure-Object -Average).Average\n                }\n                \"Total\" {\n                    $aggregatedValue = ($valuesAggregation | Measure-Object -Sum).Sum\n                }\n            }\n        }\n        \n        $customMetric = New-Object PSObject -Property @{\n            Timestamp         = $utcNow\n            Cloud             = $cloudEnvironment\n            TenantGuid        = $resource.tenantId\n            SubscriptionGuid  = $resource.subscriptionId\n            ResourceGroupName = $resource.resourceGroup.ToLower()\n            ResourceName      = $resource.name.ToLower()\n            ResourceId        = $resource.id.ToLower()\n            MetricNames       = $MetricNames\n            AggregationType   = $AggregationType\n            AggregationOfType = $AggregationOfType\n            MetricValue       = $aggregatedValue\n            TimeGrain         = $TimeGrain\n            TimeSpan          = $TimeSpan\n        }\n    \n        $customMetrics += $customMetric\n    }\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Found $($customMetrics.Count) resources to collect metrics from...\"\n\n$metricMoment = $queryDate.Add($TimeSpanObj).ToUniversalTime().ToString(\"yyyyMMddHHmmss\")\n$ResourceTypeName = $ResourceType.Split('/')[1].ToLower()\n$MetricName = $MetricNames.Replace(',','').Replace(' ','').Replace('/','').ToLower()\n$AggregationOfTypeName = \"\"\nif ($AggregationOfType -ne \"Default\")\n{\n    $AggregationOfTypeName = (\"-$AggregationOfType\").ToLower()\n}\n$AggregationTypeName = \"$($AggregationType.ToLower())$AggregationOfTypeName\"\n$csvExportPath = \"$metricMoment-metrics-$ResourceTypeName-$MetricName-$AggregationTypeName-$subscriptionSuffix.csv\"\n\n$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name)\nif ($ci.NumberFormat.NumberDecimalSeparator -ne '.')\n{\n    Write-Output \"Current culture ($($ci.Name)) does not use . as decimal separator\"    \n    $ci.NumberFormat.NumberDecimalSeparator = '.'\n    [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci\n}\n\n$customMetrics | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"\n\n"
  },
  {
    "path": "runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName,\n\n    [Parameter(Mandatory = $false)] \n    [string] $targetStartDate, # YYYY-MM-DD format\n\n    [Parameter(Mandatory = $false)] \n    [string] $targetEndDate # YYYY-MM-DD format\n)\n\n$ErrorActionPreference = \"Stop\"\n$global:hadErrors = $false\n$global:scopesWithErrors = @()\n\nfunction Authenticate-AzureWithOption {\n    param (\n        [string] $authOption = \"ManagedIdentity\",\n        [string] $cloudEnv = \"AzureCloud\",\n        [string] $clientID \n    )\n\n    switch ($authOption) {\n        \"UserAssignedManagedIdentity\" { \n            Connect-AzAccount -Identity -EnvironmentName $cloudEnv -AccountId $clientID\n            break\n        }\n        Default { #ManagedIdentity\n            Connect-AzAccount -Identity -EnvironmentName $cloudEnv \n            break\n        }\n    }\n}\n\nfunction Generate-CostDetails {\n    param (        \n        [string] $ScopeId,\n        [string] $ScopeName \n    )\n\n    $MaxTries = 20 # The typical Retry-After is set to 20 seconds. We'll give ~6 minutes overall to download the cost details report\n    $hadErrors = $false\n\n    $CostDetailsApiPath = \"$ScopeId/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2022-05-01\"\n    $body = \"{ `\"metric`\": `\"$consumptionMetric`\", `\"timePeriod`\": { `\"start`\": `\"$targetStartDate`\", `\"end`\": `\"$targetEndDate`\" } }\"\n    $result = Invoke-AzRestMethod -Path $CostDetailsApiPath -Method POST -Payload $body\n    $requestResultPath = $result.Headers.Location.PathAndQuery\n    if ($result.StatusCode -in (200,202))\n    {\n        $tries = 0\n        $requestSuccess = $false\n\n        Write-Output \"Obtained cost detail results endpoint: $requestResultPath...\"\n\n        Write-Output \"Was told to wait $($result.Headers.RetryAfter.Delta.TotalSeconds) seconds.\"\n\n        $sleepSeconds = 60\n        if ($result.Headers.RetryAfter.Delta.TotalSeconds -gt 0)\n        {\n            $sleepSeconds = $result.Headers.RetryAfter.Delta.TotalSeconds\n        }\n\n        do\n        {\n            $tries++\n            Write-Output \"Checking whether export is ready (try $tries)...\"\n            \n            Start-Sleep -Seconds $sleepSeconds\n            $downloadResult = Invoke-AzRestMethod -Method GET -Path $requestResultPath\n\n            if ($downloadResult.StatusCode -eq 200)\n            {\n\n                Write-Output \"Export is ready. Proceeding with CSV download...\"\n\n                $downloadBlobJson = $downloadResult.Content | ConvertFrom-Json\n\n                $blobCounter = 0\n                foreach ($blob in $downloadBlobJson.manifest.blobs)\n                {\n                    $blobCounter++\n\n                    Write-Output \"Downloading blob $blobCounter...\"\n\n                    $csvExportPath = \"$env:TEMP\\$targetStartDate-$ScopeName-$consumptionMetric-$blobCounter.csv\"\n                    $finalCsvExportPath = \"$env:TEMP\\$targetStartDate-$ScopeName-$consumptionMetric-$blobCounter-final.csv\"\n\n                    Invoke-WebRequest -Uri $blob.blobLink -OutFile $csvExportPath\n\n                    Write-Output \"Blob downloaded to $csvExportPath successfully.\"\n\n                    $r = [IO.File]::OpenText($csvExportPath)\n                    $w = [System.IO.StreamWriter]::new($finalCsvExportPath)\n\n                    # header normalization between MCA and EA\n                    $headerConversion = @{\n                        additionalInfo = \"AdditionalInfo\";\n                        billingAccountId = \"BillingAccountId\";\n                        billingAccountName = \"BillingAccountName\";\n                        billingCurrency = \"BillingCurrencyCode\";\n                        billingPeriodEndDate = \"BillingPeriodEndDate\";\n                        billingPeriodStartDate = \"BillingPeriodStartDate\";\n                        billingProfileId = \"BillingProfileId\";\n                        billingProfileName = \"BillingProfileName\";\n                        chargeType = \"ChargeType\";\n                        consumedService = \"ConsumedService\";\n                        costAllocationRuleName = \"CostAllocationRuleName\";\n                        costCenter = \"CostCenter\";\n                        costInBillingCurrency = \"CostInBillingCurrency\";\n                        date = \"Date\";\n                        effectivePrice = \"EffectivePrice\";\n                        frequency = \"Frequency\";\n                        invoiceSectionId = \"InvoiceSectionId\";\n                        invoiceSectionName = \"InvoiceSectionName\";\n                        isAzureCreditEligible = \"IsAzureCreditEligible\";\n                        meterCategory = \"MeterCategory\";\n                        meterId = \"MeterId\";\n                        meterName = \"MeterName\";\n                        meterRegion = \"MeterRegion\";\n                        meterSubCategory = \"MeterSubCategory\";\n                        offerId = \"OfferId\";\n                        pricingModel = \"PricingModel\";\n                        productOrderId = \"ProductOrderId\";\n                        productOrderName = \"ProductOrderName\";\n                        publisherName = \"PublisherName\";\n                        publisherType = \"PublisherType\";\n                        quantity = \"Quantity\";\n                        reservationId = \"ReservationId\";\n                        reservationName = \"ReservationName\";\n                        resourceGroupName = \"ResourceGroup\";\n                        resourceLocation = \"ResourceLocation\";\n                        serviceFamily = \"ServiceFamily\";\n                        serviceInfo1 = \"ServiceInfo1\";\n                        serviceInfo2 = \"ServiceInfo2\";\n                        subscriptionName = \"SubscriptionName\";\n                        tags = \"Tags\";\n                        term = \"Term\";\n                        unitOfMeasure = \"UnitOfMeasure\";\n                        unitPrice = \"UnitPrice\"\n                    }\n\n                    $lineCounter = 0\n                    while ($r.Peek() -ge 0) {\n                        $line = $r.ReadLine()\n                        $lineCounter++\n                        if ($lineCounter -eq 1)\n                        {\n                            $headers = $line.Split(\",\")\n\n                            for ($i = 0; $i -lt $headers.Length; $i++)\n                            {\n                                $header = $headers[$i]\n                                if ($headerConversion.ContainsKey($header))\n                                {\n                                    $headers[$i] = $headerConversion[$header]\n                                }\n                            }\n\n                            $line = $headers -join \",\"\n\n                            $w.WriteLine($line)\n                        }\n                        else\n                        {\n                            $w.WriteLine($line)\n                        }\n                    }\n                    $r.Dispose()\n                    $w.Close()        \n\n                    $csvBlobName = [System.IO.Path]::GetFileName($finalCsvExportPath)\n                    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n                    Set-AzStorageBlobContent -File $finalCsvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n                    \n                    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n                    Write-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n                \n                    Remove-Item -Path $csvExportPath -Force\n                \n                    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n                    Write-Output \"[$now] Removed $csvExportPath from local disk...\"                    \n\n                    Remove-Item -Path $finalCsvExportPath -Force\n        \n                    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n                    Write-Output \"[$now] Removed $finalCsvExportPath from local disk...\"                            \n                }\n\n                $requestSuccess = $true\n            }\n            elseif ($downloadResult.StatusCode -eq 202)\n            {\n                Write-Output \"Was told to wait a bit more... $($downloadResult.Headers.RetryAfter.Delta.TotalSeconds) seconds.\"\n\n                $sleepSeconds = 60\n                if ($downloadResult.Headers.RetryAfter.Delta.TotalSeconds -gt 0)\n                {\n                    $sleepSeconds = $downloadResult.Headers.RetryAfter.Delta.TotalSeconds\n                }\n            }\n            elseif ($downloadResult.StatusCode -eq 401)\n            {\n                Write-Output \"Had an authentication issue. Will login again and sleep just a couple of seconds.\"\n\n                if ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n                {\n                    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID\n                }\n                else\n                {    \n                    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment\n                }\n                \n                $sleepSeconds = 2\n            }\n            else\n            {\n                $global:hadErrors = $true\n                $global:scopesWithErrors += $ScopeName\n                Write-Warning \"Got an unexpected response code: $($downloadResult.StatusCode)\"\n            }\n        } \n        while (-not($requestSuccess) -and $tries -lt $MaxTries)\n\n        if (-not($requestSuccess))\n        {\n            $global:hadErrors = $true\n            $global:scopesWithErrors += $ScopeName\n            if ($tries -eq $MaxTries)\n            {\n                Write-Warning \"Reached maximum number of tries. Aborting...\"\n            }\n            else\n            {\n                Write-Warning \"Error returned by the Download Cost Details API. Status Code: $($downloadResult.StatusCode). Message: $($downloadResult.Content)\"    \n            }\n        }\n        else\n        {\n            Write-Output \"Export download processing complete.\"\n        }\n    }\n    else\n    {\n        if ($result.StatusCode -ne 204)\n        {\n            $global:hadErrors = $true\n            $global:scopesWithErrors += $ScopeName\n            Write-Warning \"Error returned by the Generate Cost Details API. Status Code: $($result.StatusCode). Message: $($result.Content)\"\n        }\n        else\n        {\n            Write-Output \"Request returned 204 No Content\"\n        }\n    }\n}\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"consumptionexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n\n$consumptionMetric = Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionMetric\" -ErrorAction SilentlyContinue # AmortizedCost|ActualCost\nif ([string]::IsNullOrEmpty($consumptionMetric))\n{\n    $consumptionMetric = \"AmortizedCost\"\n}\n\n$consumptionAPIOption = Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionAPIOption\" -ErrorAction SilentlyContinue # CostDetails|UsageDetails\nif ([string]::IsNullOrEmpty($consumptionAPIOption))\n{\n    $consumptionAPIOption = \"CostDetails\"\n}\n\n$consumptionScope = Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionScope\" -ErrorAction SilentlyContinue # Subscription|BillingAccount\nif ([string]::IsNullOrEmpty($consumptionScope))\n{\n    \"Consumption Scope not specified, defaulting to Subscription\"\n    $consumptionScope = \"Subscription\"\n}\nelse\n{\n    \"Consumption Scope is $consumptionScope\"\n    if ($consumptionScope -eq \"BillingAccount\")\n    {\n        $BillingAccountID = Get-AutomationVariable -Name  \"AzureOptimization_BillingAccountID\"        \n    }\n    else\n    {\n        if ($consumptionScope -ne \"Subscription\")\n        {\n            throw \"Invalid value for AzureOptimization_ConsumptionScope. Valid values are 'Subscription' or 'BillingAccount'.\"\n        }\n    }\n}\n\nif ($cloudEnvironment -eq \"AzureChinaCloud\")\n{\n    $chinaEAEnrollment = Get-AutomationVariable -Name \"AzureOptimization_AzureChinaEAEnrollment\" -ErrorAction SilentlyContinue    \n    $chinaEAKey = Get-AutomationVariable -Name  \"AzureOptimization_AzureChinaEAKey\" -ErrorAction SilentlyContinue\n}\n\n\"Logging in to Azure with $authenticationOption...\"\n\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID\n}\nelse\n{    \n    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n# compute start+end dates\n\nif ([string]::IsNullOrEmpty($targetStartDate) -or [string]::IsNullOrEmpty($targetEndDate))\n{\n    $targetStartDate = (Get-Date).Date.AddDays($consumptionOffsetDays * -1).ToString(\"yyyy-MM-dd\")\n    $targetEndDate = $targetStartDate    \n}\n\nif ($consumptionScope -eq \"Subscription\")\n{\n    if (-not([string]::IsNullOrEmpty($TargetSubscription)))\n    {\n        $subscriptions = Get-AzSubscription -SubscriptionId $TargetSubscription\n    }\n    else\n    {\n        $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" }\n    }    \n    \"Exporting consumption data from $targetStartDate to $targetEndDate for $($subscriptions.Count) subscriptions...\"\n}\nelse\n{\n    \"Exporting consumption data from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID...\"\n}\n\n\n# for each subscription, get billing data\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nif ($cloudEnvironment -eq \"AzureChinaCloud\" -and -not([string]::IsNullOrEmpty($chinaEAEnrollment)) -and -not([string]::IsNullOrEmpty($chinaEAKey)))\n{\n    $targetMonth = $targetStartDate.Substring(0,7)\n    $consumption = $null\n    $billingEntries = @()\n    \n    $BillingApiUri = \"https://ea.azure.cn/rest/$chinaEAEnrollment/usage-report?month=$targetMonth&type=detail&fmt=Csv\"\n    $PricesheetApiUri = \"https://ea.azure.cn/rest/$chinaEAEnrollment/usage-report?month=$targetMonth&type=pricesheet&fmt=Csv\"\n        \n    $Headers = @{}\n    $Headers.Add(\"Authorization\",\"Bearer $chinaEAKey\")\n    \n    Write-Output \"Getting pricesheet for month $targetMonth (EA enrollment $chinaEAEnrollment)...\"\n    \n    Invoke-RestMethod -Method Get -Uri $PricesheetApiUri -Headers $Headers -OutFile \"pricesheet-$targetMonth.csv\"\n    \n    Write-Output \"Pricesheet data exported to disk as CSV.\"\n    \n    $csvFile = Get-Content -Path \"pricesheet-$targetMonth.csv\"\n    \n    Write-Output \"Pricesheet data imported from disk as string.\"\n    \n    Remove-Item -Path \"pricesheet-$targetMonth.csv\" -Force\n    \n    Write-Output \"Removed pricesheet-$targetMonth.csv from local disk...\"    \n    \n    $csvFile2 = $csvFile[2..($csvFile.Count-1)]\n    $headerLine = $csvFile2[0]\n    $columnHeaders = $headerLine.Split(\",\")\n    for ($i = 0; $i -lt $columnHeaders.Count; $i++)\n    {\n        if($columnHeaders[$i] -match '.+\\((?<ColumnName>.+)\\)')\n        {\n            $columnHeaders[$i] = $Matches.ColumnName\n        }\n    }\n    $csvFile2[0] = $columnHeaders -join \",\"\n    \n    Write-Output \"Removed first 2 lines and replaced header.\"\n    \n    $pricesheet = $csvFile2 | ConvertFrom-Csv\n    \n    Write-Output \"Starting Azure China billing export process from $targetStartDate to $targetEndDate (month $targetMonth) for EA enrollment $chinaEAEnrollment...\"\n    \n    $tries = 0\n    $requestSuccess = $false\n    do \n    {\n        try {\n            $tries++\n            Invoke-RestMethod -Method Get -Uri $BillingApiUri -Headers $Headers -OutFile \"usagedetails-$targetStartDate.csv\"\n    \n            Write-Output \"Consumption data exported to disk as CSV.\"\n    \n            $csvFile = Get-Content -Path \"usagedetails-$targetStartDate.csv\"\n    \n            Write-Output \"Consumption data imported from disk as string.\"\n    \n            Remove-Item -Path \"usagedetails-$targetStartDate.csv\" -Force\n    \n            Write-Output \"Removed usagedetails-$targetStartDate.csv from local disk...\"    \n            \n            $csvFile2 = $csvFile[2..($csvFile.Count-1)]\n            $headerLine = $csvFile2[0]\n            $columnHeaders = $headerLine.Split(\",\")\n            for ($i = 0; $i -lt $columnHeaders.Count; $i++)\n            {\n                if($columnHeaders[$i] -match '.+\\((?<ColumnName>.+)\\)')\n                {\n                    $columnHeaders[$i] = $Matches.ColumnName\n                }\n            }\n            $csvFile2[0] = $columnHeaders -join \",\"\n    \n            Write-Output \"Removed first 2 lines and replaced header.\"\n    \n            $consumption = $csvFile2 | ConvertFrom-Csv  \n            $requestSuccess = $true\n        }\n        catch {\n            $ErrorMessage = $_.Exception.Message\n            Write-Warning \"Error getting consumption data: $ErrorMessage. $tries of 3 tries. Waiting 60 seconds...\"\n            Start-Sleep -s 60   \n        }\n    \n    } while ( -not($requestSuccess) -and $tries -lt 3 )\n    \n    if (-not($requestSuccess))\n    {\n        throw \"Failed consumption export\"\n    }\n    \n    Write-Output \"Consumption data in memory as CSV. Processing lines...\"\n    \n    foreach ($consumptionLine in $consumption)\n    {\n        $usageDate = [Datetime]::ParseExact($consumptionLine.Date, 'MM/dd/yyyy', $null).ToString(\"yyyy-MM-dd\")\n    \n        if ($usageDate -ge $targetStartDate -and $usageDate -le $targetEndDate -and ($subscriptions.Count -gt 1 -or $subscriptions.Id -eq $consumptionLine.SubscriptionGuid))\n        {\n            $instanceId = $null\n            $instanceName = $null\n            if ($null -ne $consumptionLine.'Instance ID')\n            {\n                $instanceId = $consumptionLine.'Instance ID'.ToLower()\n                $idParts = $consumptionLine.'Instance ID'.Split(\"/\")\n                $instanceName = $idParts[$idParts.Count-1].ToLower()\n            }\n        \n            $rgName = $null\n            if ($null -ne $consumptionLine.'Resource Group')\n            {\n                $rgName = $consumptionLine.'Resource Group'.ToLower()\n            }\n        \n            $convertedCost = 0.0    \n            if ([double]$consumptionLine.ExtendedCost -ne 0)\n            {\n                $convertedCost = [double]$consumptionLine.ExtendedCost\n            }\n            $convertedPrice = 0.0    \n            if ([double]$consumptionLine.ResourceRate -ne 0)\n            {\n                $convertedPrice = [double]$consumptionLine.ResourceRate\n            }\n    \n            $unitPrice = 0.0\n            $partNumber = \"N/A\"\n            foreach ($priceItem in $pricesheet)\n            {\n                if ($priceItem.Service -eq $consumptionLine.Product)\n                {\n                    $partNumber = $priceItem.'Part Number'\n                    if ($consumptionLine.'Meter Category' -eq \"Virtual Machines\")\n                    {\n                        $tempUnitPrice = [double] $priceItem.'Unit Price'\n                        $uom = $priceItem.'Unit of Measure'\n                        $currentUnitHours = [int] (Select-String -InputObject $uom -Pattern \"^\\d+\").Matches[0].Value\n                        if ($currentUnitHours -gt 0)\n                        {\n                            $unitPrice = [double] ($tempUnitPrice / $currentUnitHours)\n                        }    \n                    }\n                    else\n                    {\n                        $unitPrice = $convertedPrice    \n                    }\n                    break\n                }\n            }\n        \n            $billingEntry = New-Object PSObject -Property @{\n                Timestamp = $timestamp\n                SubscriptionId = $consumptionLine.SubscriptionGuid\n                ResourceGroup = $rgName\n                ResourceName = $instanceName\n                ResourceId = $instanceId\n                Date = $consumptionLine.Date\n                Tags = $consumptionLine.Tags\n                AdditionalInfo = $consumptionLine.AdditionalInfo\n                BillingCurrencyCode = \"CNY\"\n                ChargeType = \"Usage\"\n                ConsumedService = $consumptionLine.'Consumed Service'\n                CostInBillingCurrency = $convertedCost\n                EffectivePrice = $convertedPrice\n                Frequency = \"UsageBased\"\n                MeterCategory = $consumptionLine.'Meter Category'\n                MeterId = $consumptionLine.'Meter ID'\n                MeterName = $consumptionLine.'Meter Name'\n                MeterSubCategory = $consumptionLine.'Meter Sub-Category'\n                PartNumber = $partNumber\n                ProductName = $consumptionLine.Product\n                Quantity = $consumptionLine.'Consumed Quantity'\n                UnitOfMeasure = $consumptionLine.'Unit of Measure'\n                UnitPrice = $unitPrice\n                ResourceLocation = $consumptionLine.'Resource Location'\n                AccountOwnerId = $consumptionLine.AccountOwnerId\n            }\n            \n            $billingEntries += $billingEntry    \n        }\n    }    \n    \n    if ($targetStartDate -ne $targetEndDate)\n    {\n        $targetStartDate = \"$targetStartDate-$targetEndDate\"\n    }\n        \n    $csvExportPath = \"$targetStartDate-eachina.csv\"\n    \n    $billingEntries | Export-Csv -Path $csvExportPath -NoTypeInformation\n    \n    Write-Output \"Exported $($billingEntries.Count) entries as CSV to $csvExportPath\"\n    \n    $csvBlobName = $csvExportPath\n    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n    Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n    \n    Write-Output \"Uploaded to blob storage!\"\n    \n    Remove-Item -Path $csvExportPath -Force\n    \n    Write-Output \"Removed $csvExportPath from local disk...\"    \n}\nelse\n{\n    if ($consumptionScope -eq \"Subscription\")\n    {\n        $CostDetailsSupportedQuotaIDs = @('EnterpriseAgreement_2014-09-01','Internal_2014-09-01','CSP_2015-05-01')\n        $ConsumptionSupportedQuotaIDs = @('PayAsYouGo_2014-09-01','MSDN_2014-09-01')\n        \n        foreach ($subscription in $subscriptions)\n        {\n            $subscriptionQuotaID = $subscription.SubscriptionPolicies.QuotaId\n        \n            if ($subscriptionQuotaID -in $ConsumptionSupportedQuotaIDs -or $consumptionAPIOption -eq \"UsageDetails\")\n            {\n                $consumption = $null\n                $billingEntries = @()\n            \n                $ConsumptionApiPath = \"/subscriptions/$($subscription.Id)/providers/Microsoft.Consumption/usageDetails?api-version=2021-10-01&metric=$($consumptionMetric.ToLower())&%24expand=properties%2FmeterDetails%2Cproperties%2FadditionalInfo&%24filter=properties%2FusageStart%20ge%20%27$targetStartDate%27%20and%20properties%2FusageEnd%20le%20%27$targetEndDate%27\"\n            \n                \"Starting consumption export process from $targetStartDate to $targetEndDate for subscription $($subscription.Name)...\"\n            \n                do\n                {\n                    if (-not([string]::IsNullOrEmpty($consumption.nextLink)))\n                    {\n                        $ConsumptionApiPath = $consumption.nextLink.Substring($consumption.nextLink.IndexOf(\"/subscriptions/\"))\n                    }\n                    $tries = 0\n                    $requestSuccess = $false\n                    do \n                    {        \n                        try {\n                            $tries++\n                            $consumption = (Invoke-AzRestMethod -Path $ConsumptionApiPath -Method GET).Content | ConvertFrom-Json                    \n                            $requestSuccess = $true\n                        }\n                        catch {\n                            $ErrorMessage = $_.Exception.Message\n                            Write-Warning \"Error getting consumption data: $ErrorMessage. $tries of 3 tries. Waiting 60 seconds...\"\n                            Start-Sleep -s 60   \n                        }\n                    } while ( -not($requestSuccess) -and $tries -lt 3 )\n            \n                    foreach ($consumptionLine in $consumption.value)\n                    {\n                        if ((Get-Date $consumptionLine.properties.date).ToString(\"yyyy-MM-dd\") -ge $targetStartDate -and (Get-Date $consumptionLine.properties.date).ToString(\"yyyy-MM-dd\") -le $targetEndDate)\n                        {\n                            if ($consumptionLine.tags)\n                            {\n                                $tags = $consumptionLine.tags | ConvertTo-Json -Compress\n                            }\n                            else\n                            {\n                                $tags = $null\n                            }\n            \n                            $billingEntry = New-Object PSObject -Property @{\n                                Timestamp = $timestamp\n                                AccountName = $consumptionLine.properties.accountName\n                                AccountOwnerId = $consumptionLine.properties.accountOwnerId\n                                AdditionalInfo = $consumptionLine.properties.additionalInfo\n                                benefitId = $consumptionLine.properties.benefitId\n                                benefitName = $consumptionLine.properties.benefitName\n                                BillingAccountId = $consumptionLine.properties.billingAccountId\n                                BillingAccountName = $consumptionLine.properties.billingAccountName\n                                BillingCurrencyCode = $consumptionLine.properties.billingCurrency\n                                BillingPeriodEndDate= $consumptionLine.properties.billingPeriodEndDate\n                                BillingPeriodStartDate= $consumptionLine.properties.billingPeriodStartDate\n                                BillingProfileId = $consumptionLine.properties.billingProfileId\n                                BillingProfileName= $consumptionLine.properties.billingProfileName\n                                ChargeType = $consumptionLine.properties.chargeType\n                                ConsumedService = $consumptionLine.properties.consumedService\n                                CostAllocationRuleName = $consumptionLine.properties.costAllocationRuleName\n                                CostCenter = $consumptionLine.properties.costCenter\n                                CostInBillingCurrency = $consumptionLine.properties.cost\n                                Date = (Get-Date $consumptionLine.properties.date).ToString(\"MM/dd/yyyy\")\n                                EffectivePrice = $consumptionLine.properties.effectivePrice\n                                Frequency = $consumptionLine.properties.frequency\n                                InvoiceSectionName = $consumptionLine.properties.invoiceSection\n                                IsAzureCreditEligible = $consumptionLine.properties.isAzureCreditEligible\n                                MeterCategory = $consumptionLine.properties.meterDetails.meterCategory\n                                MeterId = $consumptionLine.properties.meterId\n                                MeterName = $consumptionLine.properties.meterDetails.meterName\n                                MeterRegion = $consumptionLine.properties.meterDetails.meterRegion\n                                MeterSubCategory = $consumptionLine.properties.meterDetails.meterSubCategory\n                                OfferId = $consumptionLine.properties.offerId\n                                PartNumber = $consumptionLine.properties.partNumber\n                                PayGPrice = $consumptionLine.properties.PayGPrice\n                                PlanName = $consumptionLine.properties.planName\n                                PricingModel = $consumptionLine.properties.pricingModel\n                                ProductName = $consumptionLine.properties.product\n                                PublisherName = $consumptionLine.properties.publisherName\n                                PublisherType = $consumptionLine.properties.publisherType\n                                Quantity = $consumptionLine.properties.quantity\n                                ReservationId = $consumptionLine.properties.reservationId\n                                ReservationName = $consumptionLine.properties.reservationName\n                                ResourceGroup = $consumptionLine.properties.resourceGroup\n                                ResourceId = $consumptionLine.properties.resourceId\n                                ResourceLocation = $consumptionLine.properties.resourceLocation\n                                ResourceName = $consumptionLine.properties.resourceName\n                                ServiceFamily = $consumptionLine.properties.meterDetails.serviceFamily\n                                SubscriptionId = $consumptionLine.properties.subscriptionId\n                                SubscriptionName = $consumptionLine.properties.subscriptionName\n                                Tags = $tags\n                                Term = $consumptionLine.properties.term\n                                UnitOfMeasure = $consumptionLine.properties.meterDetails.unitOfMeasure\n                                UnitPrice = $consumptionLine.properties.unitPrice\n                            }            \n                            $billingEntries += $billingEntry    \n                        }\n                    }    \n                }\n                while ($requestSuccess -and -not([string]::IsNullOrEmpty($consumption.nextLink)))\n            \n                if ($requestSuccess)\n                {\n                    \"Generated $($billingEntries.Count) entries...\"\n                \n                    \"Uploading CSV to Storage\"\n                \n                    $ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name)\n                    if ($ci.NumberFormat.NumberDecimalSeparator -ne '.')\n                    {\n                        \"Current culture ($($ci.Name)) does not use . as decimal separator\"    \n                        $ci.NumberFormat.NumberDecimalSeparator = '.'\n                        [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci\n                    }\n                \n                    $csvExportPath = \"$targetStartDate-$($subscription.Id)-$consumptionMetric.csv\"\n            \n                    $billingEntries | Export-Csv -Path $csvExportPath -NoTypeInformation    \n            \n                    $csvBlobName = $csvExportPath\n                    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n                    Set-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n                    \n                    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n                    \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n                \n                    Remove-Item -Path $csvExportPath -Force\n                \n                    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n                    \"[$now] Removed $csvExportPath from local disk...\"        \n                }\n                else\n                {\n                    $global:hadErrors = $true\n                    $global:scopesWithErrors += $ScopeName\n                    Write-Warning \"Failed to get consumption data for subscription $($subscription.Name)...\"\n                }\n            }\n            elseif ($subscriptionQuotaID -in $CostDetailsSupportedQuotaIDs -or $consumptionAPIOption -eq \"CostDetails\")\n            {\n                \"Starting cost details export process from $targetStartDate to $targetEndDate for subscription $($subscription.Name)...\"\n                Generate-CostDetails -ScopeId \"/subscriptions/$($subscription.Id)\" -ScopeName $subscription.Id\n            }\n            else\n            {\n                $global:hadErrors = $true\n                $global:scopesWithErrors += $ScopeName\n                Write-Warning \"Subscription quota $subscriptionQuotaID not supported\"\n            }\n        }    \n    }\n    else\n    {\n        \"Starting cost details export process from $targetStartDate to $targetEndDate for Billing Account ID $BillingAccountID...\"\n        Generate-CostDetails -ScopeId \"/providers/Microsoft.Billing/billingAccounts/$BillingAccountID\" -ScopeName $BillingAccountID\n    }    \n}\n\nif ($global:hadErrors)\n{\n    $scopesWithErrorsString = $global:scopesWithErrors -join \",\"\n    throw \"There were errors during the export process with the following scopes: $scopesWithErrorsString. Please check the output for details.\"\n}"
  },
  {
    "path": "runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetSubscription,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName,\n\n    [Parameter(Mandatory = $false)]\n    [ValidateSet(\"ARG\", \"ARM\")]\n    [string] $PolicyStatesEndpoint = \"ARG\"\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\" -ErrorAction SilentlyContinue # e.g., westeurope\nif ([string]::IsNullOrEmpty($referenceRegion))\n{\n    $referenceRegion = \"westeurope\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_PolicyStatesContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"policystateexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$ARGPageSize = 1000\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\n$cloudSuffix = \"\"\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudSuffix = $externalCloudEnvironment.ToLower() + \"-\"\n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n$allpolicyStates = @()\n\nWrite-Output \"Getting subscriptions target $TargetSubscription\"\nif (-not([string]::IsNullOrEmpty($TargetSubscription)))\n{\n    $subscriptions = $TargetSubscription\n    $subscriptionSuffix = $TargetSubscription\n}\nelse\n{\n    $subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" -and $_.SubscriptionPolicies.QuotaId -notlike \"AAD*\" } | ForEach-Object { \"$($_.Id)\"}\n    $subscriptionSuffix = $cloudSuffix + \"all-\" + $tenantId\n}\n\nWrite-Output \"Building Policy display names...\"\n\n$policyAssignments = @{}\n$policyInitiatives = @{}\n$policyDefinitions = @{}\n$excludedAssignmentScopes = @()\n$allInitiatives = @()\n\nif ($PolicyStatesEndpoint -eq \"ARG\")\n{\n    $resultsSoFar = 0\n\n    $argQuery = @\"\n    policyresources\n    | where type =~ 'microsoft.authorization/policyassignments'\n    | extend displayName = iif(isnotempty(properties.displayName), tostring(properties.displayName), 'N/A')\n    | distinct id, displayName\n    | order by id asc\n\"@\n\n    $argAssignmentsTotal = @()\n\n    do\n    {\n        if ($resultsSoFar -eq 0)\n        {\n            $argAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope\n        }\n        else\n        {\n            $argAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope\n        }\n        if ($argAssignments -and $argAssignments.GetType().Name -eq \"PSResourceGraphResponse\")\n        {\n            $argAssignments = $argAssignments.Data\n        }\n        $resultsCount = $argAssignments.Count\n        $resultsSoFar += $resultsCount\n        $argAssignmentsTotal += $argAssignments\n\n    } while ($resultsCount -eq $ARGPageSize)\n\n    Write-Output \"Building $($argAssignmentsTotal.Count) assignment entries\"\n\n    foreach ($assignment in $argAssignmentsTotal)\n    {\n        $policyAssignments.Add($assignment.id, $assignment.displayName)\n    }\n\n    $resultsSoFar = 0\n\n    $argQuery = @\"\n    policyresources\n    | where type =~ 'microsoft.authorization/policysetdefinitions'\n    | extend displayName = iif(isnotempty(properties.displayName), tostring(properties.displayName), 'N/A')\n    | distinct id, displayName\n    | order by id asc\n\"@\n\n    $argInitiativesTotal = @()\n\n    do\n    {\n        if ($resultsSoFar -eq 0)\n        {\n            $argInitiatives = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope\n        }\n        else\n        {\n            $argInitiatives = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope\n        }\n        if ($argInitiatives -and $argInitiatives.GetType().Name -eq \"PSResourceGraphResponse\")\n        {\n            $argInitiatives = $argInitiatives.Data\n        }\n        $resultsCount = $argInitiatives.Count\n        $resultsSoFar += $resultsCount\n        $argInitiativesTotal += $argInitiatives\n\n    } while ($resultsCount -eq $ARGPageSize)\n\n    Write-Output \"Building $($argInitiativesTotal.Count) initiative entries\"\n\n    foreach ($initiative in $argInitiativesTotal)\n    {\n        $policyInitiatives.Add($initiative.id, $initiative.displayName)\n    }\n\n    $resultsSoFar = 0\n\n    $argQuery = @\"\n    policyresources\n    | where type =~ 'microsoft.authorization/policydefinitions'\n    | extend displayName = iif(isnotempty(properties.displayName), tostring(properties.displayName), 'N/A')\n    | distinct id, displayName\n    | order by id asc\n\"@\n\n    $argDefinitionsTotal = @()\n\n    do\n    {\n        if ($resultsSoFar -eq 0)\n        {\n            $argDefinitions = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope\n        }\n        else\n        {\n            $argDefinitions = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope\n        }\n        if ($argDefinitions -and $argDefinitions.GetType().Name -eq \"PSResourceGraphResponse\")\n        {\n            $argDefinitions = $argDefinitions.Data\n        }\n        $resultsCount = $argDefinitions.Count\n        $resultsSoFar += $resultsCount\n        $argDefinitionsTotal += $argDefinitions\n\n    } while ($resultsCount -eq $ARGPageSize)\n\n    Write-Output \"Building $($argDefinitionsTotal.Count) definition entries\"\n\n    foreach ($definition in $argDefinitionsTotal)\n    {\n        $policyDefinitions.Add($definition.id, $definition.displayName)\n    }\n}\nelse\n{\n    foreach ($sub in $subscriptions)\n    {\n        Select-AzSubscription -SubscriptionId $sub | Out-Null\n        $assignments = Get-AzPolicyAssignment -IncludeDescendent\n        foreach ($assignment in $assignments)\n        {\n            if (-not($policyAssignments[$assignment.PolicyAssignmentId]))\n            {\n                $assignmentName = $assignment.Properties.DisplayName\n                if([string]::IsNullOrWhiteSpace($assignmentName)) {\n                    $policyAssignments.Add($assignment.PolicyAssignmentId, 'N/A')\n                }\n                else  {\n                    $policyAssignments.Add($assignment.PolicyAssignmentId, $assignmentName)\n                }\n            }\n            if ($assignment.Properties.NotScopes -and -not($excludedAssignmentScopes | Where-Object { $_.PolicyAssignmentId -eq $assignment.PolicyAssignmentId }))\n            {\n                $excludedAssignmentScopes += $assignment\n            }\n        }\n\n        $initiatives = Get-AzPolicySetDefinition\n        foreach ($initiative in $initiatives)\n        {\n            if (-not($policyInitiatives[$initiative.PolicySetDefinitionId]))\n            {\n                $setDefinitionName = $initiative.Properties.DisplayName\n                if([string]::IsNullOrWhiteSpace($setDefinitionName)) {\n                    $policyInitiatives.Add($initiative.PolicySetDefinitionId, 'N/A')\n                }\n                else  {\n                    $policyInitiatives.Add($initiative.PolicySetDefinitionId, $setDefinitionName)\n                }\n            }\n            if (-not($allInitiatives | Where-Object { $_.PolicySetDefinitionId -eq $initiative.PolicySetDefinitionId }))\n            {\n                $allInitiatives += $initiative\n            }\n        }\n\n        $definitions = Get-AzPolicyDefinition\n        foreach ($definition in $definitions)\n        {\n            if (-not($policyDefinitions[$definition.PolicyDefinitionId]))\n            {\n                $definitionName = $initiative.Properties.DisplayName\n                if([string]::IsNullOrWhiteSpace($definitionName)) {\n                    $policyDefinitions.Add($definition.PolicyDefinitionId, 'N/A')\n                }\n                else  {\n                    $policyDefinitions.Add($definition.PolicyDefinitionId, $definitionName)\n                }\n            }\n        }\n    }\n}\n\n$policyStatesTotal = @()\n\nWrite-Output \"Querying for Policy states using $PolicyStatesEndpoint endpoint...\"\n\nif ($PolicyStatesEndpoint -eq \"ARG\")\n{\n    $resultsSoFar = 0\n\n    $argQuery = @\"\n    policyresources\n    | where type =~ 'microsoft.policyinsights/policystates'\n    | extend complianceState = tostring(properties.complianceState)\n    | extend complianceReason = tostring(properties.complianceReasonCode)\n    | where complianceState != 'Compliant' and complianceReason !contains 'ResourceNotFound'\n    | extend effect = tostring(properties.policyDefinitionAction)\n    | extend assignmentId = tolower(properties.policyAssignmentId)\n    | extend definitionId = tolower(properties.policyDefinitionId)\n    | extend definitionReferenceId = tolower(properties.policyDefinitionReferenceId)\n    | extend initiativeId = tolower(properties.policySetDefinitionId)\n    | extend resourceId = tolower(properties.resourceId)\n    | extend resourceType = tostring(properties.resourceType)\n    | extend evaluatedOn = todatetime(properties.timestamp)\n    | summarize StatesCount = count() by id, tenantId, subscriptionId, resourceGroup, resourceId, resourceType, complianceState, complianceReason, effect, assignmentId, definitionReferenceId, definitionId, initiativeId, evaluatedOn\n    | union ( policyresources\n        | where type =~ 'microsoft.policyinsights/policystates'\n        | extend complianceState = tostring(properties.complianceState)\n        | where complianceState == 'Compliant'\n        | extend effect = tostring(properties.policyDefinitionAction)\n        | extend assignmentId = tolower(properties.policyAssignmentId)\n        | extend definitionId = tolower(properties.policyDefinitionId)\n        | extend definitionReferenceId = tolower(properties.policyDefinitionReferenceId)\n        | extend initiativeId = tolower(properties.policySetDefinitionId)\n        | summarize StatesCount = count() by tenantId, subscriptionId, complianceState, effect, assignmentId, definitionReferenceId, definitionId, initiativeId\n    )\n    | join kind=leftouter ( \n        resources\n        | project resourceId=tolower(id), tags\n    ) on resourceId\n    | project-away resourceId1\n    | order by id asc\n\"@\n\n    do\n    {\n        if ($resultsSoFar -eq 0)\n        {\n            $policyStates = Search-AzGraph -Query $argQuery -First $ARGPageSize -Subscription $subscriptions\n        }\n        else\n        {\n            $policyStates = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -Subscription $subscriptions\n        }\n        if ($policyStates -and $policyStates.GetType().Name -eq \"PSResourceGraphResponse\")\n        {\n            $policyStates = $policyStates.Data\n        }\n        $resultsCount = $policyStates.Count\n        $resultsSoFar += $resultsCount\n        $policyStatesTotal += $policyStates\n\n    } while ($resultsCount -eq $ARGPageSize)\n\n    Write-Output \"Building $($policyStatesTotal.Count) policyState entries\"\n}\nelse\n{\n    foreach ($sub in $subscriptions)\n    {\n        Select-AzSubscription -SubscriptionId $sub | Out-Null\n        $policyStates = Get-AzPolicyState -All\n\n        $nonCompliantStates = $policyStates | Where-Object { $_.ComplianceState -ne \"Compliant\" }\n\n        foreach ($policyState in $nonCompliantStates)\n        {\n            $policyStateObject = New-Object PSObject -Property @{\n                tenantId = $tenantId\n                subscriptionId = $sub\n                resourceGroup = $policyState.ResourceGroup\n                resourceId = $policyState.ResourceId\n                resourceType = $policyState.ResourceType\n                complianceState = $policyState.ComplianceState\n                complianceReason = $policyState.AdditionalProperties.complianceReasonCode\n                effect = $policyState.PolicyDefinitionAction\n                assignmentId = $policyState.PolicyAssignmentId\n                initiativeId = $policyState.PolicySetDefinitionId\n                definitionId = $policyState.PolicyDefinitionId\n                definitionReferenceId = $policyState.PolicyDefinitionReferenceId\n                evaluatedOn = $policyState.Timestamp\n                StatesCount = 1\n            }\n            $policyStatesTotal += $policyStateObject    \n        }\n\n        $compliantStates = $policyStates | Where-Object { $_.ComplianceState -eq \"Compliant\" } `\n            | Group-Object PolicyDefinitionAction, PolicyAssignmentId, PolicyDefinitionId, PolicyDefinitionReferenceId, PolicySetDefinitionId\n\n        foreach ($policyState in $compliantStates)\n        {\n            $compliantStateProps = $policyState.Name.Split(',')\n            $definitionReferenceId = $null\n            if ($compliantStateProps[3])\n            {\n                $definitionReferenceId = $compliantStateProps[3].Trim().ToLower()\n            }\n            $initiativeId = $null\n            if ($compliantStateProps[4])\n            {\n                $initiativeId = $compliantStateProps[4].Trim().ToLower()\n            }\n            \n            $policyStateObject = New-Object PSObject -Property @{\n                tenantId = $tenantId\n                subscriptionId = $sub\n                complianceState = \"Compliant\"\n                effect = $compliantStateProps[0]\n                assignmentId = $compliantStateProps[1].Trim().ToLower()\n                definitionId = $compliantStateProps[2].Trim().ToLower()\n                definitionReferenceId = $definitionReferenceId\n                initiativeId = $initiativeId\n                StatesCount = $policyState.Count\n            }\n            $policyStatesTotal += $policyStateObject    \n        }\n    }        \n\n    Write-Output \"Building $($policyStatesTotal.Count) policyState entries\"\n}\n\n$datetime = (Get-Date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n$statusDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nforeach ($policyState in $policyStatesTotal)\n{\n    $resourceGroup = $null\n    if ($policyState.resourceGroup)\n    {\n        $resourceGroup = $policyState.resourceGroup.ToLower()\n    }\n\n    if (-not([string]::IsNullOrEmpty($policyState.tags)))\n    {\n        $tags = $policyState.tags | ConvertTo-Json -Compress\n    }\n    else\n    {\n        $tags = $null\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $cloudEnvironment\n        TenantGuid = $policyState.tenantId\n        SubscriptionGuid = $policyState.subscriptionId\n        ResourceGroupName = $resourceGroup\n        ResourceId = $policyState.resourceId\n        ResourceType = $policyState.resourceType\n        ComplianceState = $policyState.complianceState\n        ComplianceReason = $policyState.complianceReason\n        Effect = $policyState.effect\n        AssignmentId = $policyState.assignmentId\n        AssignmentName = $policyAssignments[$policyState.assignmentId]\n        InitiativeId = $policyState.initiativeId\n        InitiativeName = $policyInitiatives[$policyState.initiativeId]\n        DefinitionId = $policyState.definitionId\n        DefinitionName = $policyDefinitions[$policyState.definitionId]\n        DefinitionReferenceId = $policyState.definitionReferenceId\n        EvaluatedOn = $policyState.evaluatedOn\n        StatesCount = $policyState.StatesCount\n        Tags = $tags\n        StatusDate = $statusDate\n    }\n    \n    $allpolicyStates += $logentry\n}\n\nif ($PolicyStatesEndpoint -eq \"ARG\")\n{\n    $resultsSoFar = 0\n\n    $argQuery = @\"\n    policyresources\n    | where type =~ 'microsoft.authorization/policyassignments'\n    | where array_length(properties.notScopes) > 0\n    | mv-expand notScope = properties.notScopes\n    | extend policyAssignmentId = tolower(id)\n    | extend assignmentPolicyDefinitionId = tolower(properties.policyDefinitionId)\n    | join kind=leftouter ( \n        policyresources\n        | where type =~ 'microsoft.authorization/policysetdefinitions'\n        | mv-expand policyDefinition = properties.policyDefinitions\n        | project policySetDefinitionId = tolower(id), policyDefinitionId = tolower(policyDefinition.policyDefinitionId), policyDefinitionReferenceId = tolower(policyDefinition.policyDefinitionReferenceId)\n    ) on `$left.assignmentPolicyDefinitionId == `$right.policySetDefinitionId\n    | project policyAssignmentId, notScope, assignmentPolicyDefinitionId, policySetDefinitionId, policyDefinitionId, policyDefinitionReferenceId\n    | order by policyDefinitionReferenceId, tostring(notScope)\n\"@\n\n    do\n    {\n        if ($resultsSoFar -eq 0)\n        {\n            $argExcludedAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -UseTenantScope\n        }\n        else\n        {\n            $argExcludedAssignments = Search-AzGraph -Query $argQuery -First $ARGPageSize -Skip $resultsSoFar -UseTenantScope\n        }\n        if ($argExcludedAssignments -and $argExcludedAssignments.GetType().Name -eq \"PSResourceGraphResponse\")\n        {\n            $argExcludedAssignments = $argExcludedAssignments.Data\n        }\n        $resultsCount = $argExcludedAssignments.Count\n        $resultsSoFar += $resultsCount\n        $excludedAssignmentScopes += $argExcludedAssignments\n\n    } while ($resultsCount -eq $ARGPageSize)\n\n    Write-Output \"Adding excluded scopes from $($excludedAssignmentScopes.Count) assignments\"\n\n    foreach ($excludedAssignmentScope in $excludedAssignmentScopes)\n    {\n        if (-not([String]::IsNullOrEmpty($excludedAssignmentScope.policySetDefinitionId)))\n        {\n            $initiativeId = $excludedAssignmentScope.policySetDefinitionId\n            $initiativeName = $policyInitiatives[$initiativeId]\n            $definitionReferenceId = $excludedAssignmentScope.policyDefinitionReferenceId\n            $definitionId = $excludedAssignmentScope.policyDefinitionId\n        }\n        else\n        {\n            $initiativeId = $null\n            $initiativeName = $null\n            $definitionReferenceId = $null\n            $definitionId = $excludedAssignmentScope.assignmentPolicyDefinitionId\n        }\n\n        $logentry = New-Object PSObject -Property @{\n            Timestamp = $timestamp\n            Cloud = $cloudEnvironment\n            TenantGuid = $tenantId\n            ResourceId = $excludedAssignmentScope.notScope\n            ComplianceState = 'Excluded'\n            AssignmentId = $excludedAssignmentScope.policyAssignmentId\n            AssignmentName = $policyAssignments[$excludedAssignmentScope.policyAssignmentId]\n            InitiativeId = $initiativeId\n            InitiativeName = $initiativeName\n            DefinitionId = $definitionId\n            DefinitionName = $policyDefinitions[$definitionId]\n            DefinitionReferenceId = $definitionReferenceId\n            StatusDate = $statusDate\n        }        \n\n        $allpolicyStates += $logentry\n    }\n}\nelse \n{\n    Write-Output \"Adding excluded scopes from $($excludedAssignmentScopes.Count) assignments\"\n\n    foreach ($excludedAssignment in $excludedAssignmentScopes)\n    {\n        $excludedIDs = @()\n        $excludedInitiative = $allInitiatives | Where-Object { $_.PolicySetDefinitionId -eq $excludedAssignment.Properties.PolicyDefinitionId }\n        if ($excludedInitiative)\n        {\n            $excludedDefinitions = $excludedInitiative.Properties.PolicyDefinitions\n            foreach ($excludedDefinition in $excludedDefinitions)\n            {\n                $excludedIDs += \"$($excludedDefinition.policyDefinitionId)|$($excludedDefinition.policyDefinitionReferenceId)\"\n            }\n        }\n        else\n        {\n            $excludedIDs += $excludedAssignment.Properties.PolicyDefinitionId\n        }\n\n        foreach ($excludedID in $excludedIDs)\n        {\n            $excludedIDParts = $excludedID.Split('|')\n            $definitionId = $excludedIDParts[0].ToLower()\n            $definitionReferenceId = $null\n            if (-not([string]::IsNullOrEmpty($excludedIDParts[1])))\n            {\n                $definitionReferenceId = $excludedIDParts[1].ToLower()\n            }\n\n            $initiativeId = $null\n            $initiativeName = $null\n            if ($excludedInitiative)\n            {\n                $initiativeId = $excludedInitiative.PolicySetDefinitionId.ToLower()\n                $initiativeName = $policyInitiatives[$initiativeId]\n            }\n\n            foreach ($notScope in $excludedAssignment.Properties.NotScopes)\n            {\n                $logentry = New-Object PSObject -Property @{\n                    Timestamp = $timestamp\n                    Cloud = $cloudEnvironment\n                    TenantGuid = $tenantId\n                    ResourceId = $notScope.ToLower()\n                    ComplianceState = 'Excluded'\n                    AssignmentId = $excludedAssignment.PolicyAssignmentId.ToLower()\n                    AssignmentName = $policyAssignments[$excludedAssignment.PolicyAssignmentId]\n                    InitiativeId = $initiativeId\n                    InitiativeName = $initiativeName\n                    DefinitionId = $definitionId\n                    DefinitionName = $policyDefinitions[$definitionId]\n                    DefinitionReferenceId = $definitionReferenceId\n                    StatusDate = $statusDate\n                }        \n\n                $allpolicyStates += $logentry\n            }\n        }\n    }\n}\n\nWrite-Output \"Uploading CSV to Storage\"\n\n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-policyStates-$subscriptionSuffix.csv\"\n\n$allpolicyStates | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\nWrite-Output \"Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\nWrite-Output \"Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $BillingAccountID,\n\n    [Parameter(Mandatory = $false)]\n    [string] $BillingProfileID,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName,\n\n    [Parameter(Mandatory = $false)] \n    [string] $billingPeriod, # YYYYMM format\n\n    [Parameter(Mandatory = $false)] \n    [string] $meterCategories, # comma-separated meter categories (e.g., \"Virtual Machines,Storage\")\n\n    [Parameter(Mandatory = $false)] \n    [string] $meterRegions # comma-separated billing meter regions (e.g., \"EU North,EU West\")\n)\n\n$ErrorActionPreference = \"Stop\"\n\nfunction Authenticate-AzureWithOption {\n    param (\n        [string] $authOption = \"ManagedIdentity\",\n        [string] $cloudEnv = \"AzureCloud\",\n        [string] $clientID \n    )\n\n    switch ($authOption) {\n        \"UserAssignedManagedIdentity\" { \n            Connect-AzAccount -Identity -EnvironmentName $cloudEnv -AccountId $clientID\n            break\n        }\n        Default { #ManagedIdentity\n            Connect-AzAccount -Identity -EnvironmentName $cloudEnv \n            break\n        }\n    }\n}\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_PriceSheetContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"pricesheetexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$meterCategoriesVar = Get-AutomationVariable -Name \"AzureOptimization_PriceSheetMeterCategories\" -ErrorAction SilentlyContinue\n$meterRegionsVar = Get-AutomationVariable -Name \"AzureOptimization_PriceSheetMeterRegions\" -ErrorAction SilentlyContinue\n$BillingAccountIDVar = Get-AutomationVariable -Name  \"AzureOptimization_BillingAccountID\" -ErrorAction SilentlyContinue\n$BillingProfileIDVar = Get-AutomationVariable -Name  \"AzureOptimization_BillingProfileID\" -ErrorAction SilentlyContinue\n\n\"Logging in to Azure with $authenticationOption...\"\n\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID\n}\nelse\n{    \n    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n# compute billing period\n\nif ([string]::IsNullOrEmpty($billingPeriod))\n{\n    $billingPeriod = (Get-Date).Date.AddDays($consumptionOffsetDays * -1).ToString(\"yyyyMM\")\n}\n\n$exportDate = (Get-Date).ToUniversalTime().ToString(\"yyyyMMdd\")\n\nif ([string]::IsNullOrEmpty($BillingAccountID) -and -not([string]::IsNullOrEmpty($BillingAccountIDVar)))\n{\n    $BillingAccountID = $BillingAccountIDVar\n}\n\nif ([string]::IsNullOrEmpty($BillingProfileID) -and -not([string]::IsNullOrEmpty($BillingProfileIDVar)))\n{\n    $BillingProfileID = $BillingProfileIDVar\n}\n\n$mcaBillingAccountIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n$mcaBillingProfileIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n\nif ([string]::IsNullOrEmpty($BillingAccountID))\n{\n    throw \"Billing Account ID undefined. Use either the AzureOptimization_BillingAccountID variable or the BillingAccountID parameter\"\n}\nelse {\n    if ($BillingAccountID -match $mcaBillingAccountIdRegex)\n    {\n        if ([string]::IsNullOrEmpty($BillingProfileID))\n        {\n            throw \"Billing Profile ID undefined for MCA. Use either the AzureOptimization_BillingProfileID variable or the BillingProfileID parameter\"\n        }\n        if (-not($BillingProfileID -match $mcaBillingProfileIdRegex))\n        {\n            throw \"Billing Profile ID does not follow pattern for MCA: ([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n        }\n    }\n}\n\nif (-not([string]::IsNullOrEmpty($meterCategoriesVar)))\n{\n    $meterCategories = $meterCategoriesVar\n}\n\nif (-not([string]::IsNullOrEmpty($meterRegionsVar)))\n{\n    $meterRegions = $meterRegionsVar\n}\n\n$meterCategoryFilters = $null\n$meterRegionFilters = $null\n\nif (-not([string]::IsNullOrEmpty($meterCategories)))\n{\n    $meterCategoryFilters = $meterCategories.Split(',')\n}\n\nif (-not([string]::IsNullOrEmpty($meterRegions)))\n{\n    $meterRegionFilters = $meterRegions.Split(',')\n}\n\nfunction Generate-Pricesheet {\n    param (        \n        [string] $InputCSVPath,\n        [string] $OutputCSVPath,\n        [string] $HeaderLine\n    )\n\n    # header normalization between MCA and EA\n    $headerConversion = @{\n        'Meter ID' = \"MeterID\";\n        meterId = \"MeterID\";\n        'Meter name' = \"MeterName\";\n        meterName = \"MeterName\";\n        'Meter category' = \"MeterCategory\";\n        meterCategory = \"MeterCategory\";\n        'Meter sub-category' = \"MeterSubCategory\";\n        meterSubCategory = \"MeterSubCategory\";\n        'Meter region' = \"MeterRegion\";\n        meterRegion = \"MeterRegion\";\n        'Unit of measure' = \"UnitOfMeasure\";\n        unitOfMeasure = \"UnitOfMeasure\";\n        'Part number' = \"PartNumber\";\n        'Unit price' = \"UnitPrice\";\n        unitPrice = \"UnitPrice\";\n        'Currency code' = \"CurrencyCode\";\n        currency = \"CurrencyCode\";\n        'Included quantity' = \"IncludedQuantity\";\n        includedQuantity = \"IncludedQuantity\";\n        'Offer Id' = \"OfferId\";\n        Term = \"Term\";\n        'Price type' = \"PriceType\";\n        priceType = \"PriceType\"\n    }\n\n    $r = [IO.File]::OpenText($InputCSVPath)\n    $w = [System.IO.StreamWriter]::new($OutputCSVPath)\n    $lineCounter = 0\n    while ($r.Peek() -ge 0) {\n        $line = $r.ReadLine()\n        $lineCounter++\n        if ($lineCounter -eq $HeaderLine)\n        {\n            $headers = $line.Split(\",\")\n\n            for ($i = 0; $i -lt $headers.Length; $i++)\n            {\n                $header = $headers[$i]\n                if ($headerConversion.ContainsKey($header))\n                {\n                    $headers[$i] = $headerConversion[$header]\n                }\n            }\n\n            $line = $headers -join \",\"\n\n            if (-not($line -match \"SubCategory\"))\n            {\n                throw \"Pricesheet format has changed at line $HeaderLine - $line\"\n            }\n\n            Write-Output \"New headers: $line\"\n\n            $w.WriteLine($line)\n        }\n        else\n        {\n            if ($lineCounter -gt $HeaderLine)\n            {\n                $categoryWriteLine = $categoryWriteLineDefault\n                $regionWriteLine = $regionWriteLineDefault\n\n                foreach ($meterCategory in $meterCategoryFilters)\n                {\n                    if ($line -match \",$meterCategory,\")\n                    {\n                        $categoryWriteLine = $true\n                        break\n                    }\n                }    \n\n                foreach ($meterRegion in $meterRegionFilters)\n                {\n                    if ($line -match \",$meterRegion,\")\n                    {\n                        $regionWriteLine = $true\n                        break\n                    }\n                }    \n\n                if ($categoryWriteLine -eq $true -and $regionWriteLine -eq $true)\n                {\n                    $w.WriteLine($line)\n                }\n            }\n        }\n    }\n    $r.Dispose()\n    $w.Close()\n\n    $csvBlobName = [System.IO.Path]::GetFileName($OutputCSVPath)\n    $csvProperties = @{\"ContentType\" = \"text/csv\"};\n    Set-AzStorageBlobContent -File $OutputCSVPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n        \n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    Write-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\n    Remove-Item -Path $InputCSVPath -Force\n\n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    Write-Output \"[$now] Removed $InputCSVPath from local disk...\"                    \n\n    Remove-Item -Path $OutputCSVPath -Force\n\n    $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    Write-Output \"[$now] Removed $OutputCSVPath from local disk...\"                    \n}\n\nWrite-Output \"Starting pricesheet export process for $billingPeriod billing period for Billing Account $BillingAccountID...\"\n\n$MaxTries = 30 # The typical Retry-After is set to 20 seconds. We'll give 10 minutes overall to download the pricesheet report\n\nif ($BillingAccountID -match $mcaBillingAccountIdRegex)\n{\n    $PriceSheetApiPath = \"/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingProfiles/$BillingProfileID/providers/Microsoft.CostManagement/pricesheets/default/download?api-version=2023-03-01&format=csv\"\n    $result = Invoke-AzRestMethod -Path $PriceSheetApiPath -Method POST\n}\nelse\n{\n    $PriceSheetApiPath = \"/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingPeriods/$billingPeriod/providers/Microsoft.Consumption/pricesheets/download?api-version=2022-06-01&ln=en\"\n    $result = Invoke-AzRestMethod -Path $PriceSheetApiPath -Method GET\n}\n\n$requestResultPath = $result.Headers.Location.PathAndQuery\nif ($result.StatusCode -in (200,202))\n{\n    $tries = 0\n    $requestSuccess = $false\n\n    Write-Output \"Obtained pricesheet results endpoint: $requestResultPath...\"\n\n    Write-Output \"Was told to wait $($result.Headers.RetryAfter.Delta.TotalSeconds) seconds.\"\n\n    $sleepSeconds = 60\n    if ($result.Headers.RetryAfter.Delta.TotalSeconds -gt 0)\n    {\n        $sleepSeconds = $result.Headers.RetryAfter.Delta.TotalSeconds\n    }\n\n    do\n    {\n        $tries++\n        Write-Output \"Checking whether export is ready (try $tries)...\"\n        \n        Start-Sleep -Seconds $sleepSeconds\n        $downloadResult = Invoke-AzRestMethod -Method GET -Path $requestResultPath\n\n        if ($downloadResult.StatusCode -eq 200)\n        {\n            Write-Output \"Filtering data with meter categories $meterCategories and meter regions $meterRegions to $finalCsvExportPath...\"\n\n            $categoryWriteLineDefault = $true\n            if ($meterCategoryFilters.Count -gt 0)\n            {\n                $categoryWriteLineDefault = $false\n            }\n            $regionWriteLineDefault = $true\n            if ($meterRegionFilters.Count -gt 0)\n            {\n                $regionWriteLineDefault = $false\n            }\n\n            Write-Output \"Defaulting to meter categories writes $($categoryWriteLineDefault) and meter regions writes $($regionWriteLineDefault)...\"\n\n            if ($BillingAccountID -match $mcaBillingAccountIdRegex)\n            {\n                Write-Output \"Export is ready. Proceeding with ZIP download...\"\n                $downloadUrl = ($downloadResult.Content | ConvertFrom-Json).publishedEntity.properties.downloadUrl\n                $zipExportPath = \"$env:TEMP\\pricesheet-$BillingProfileID-$exportDate.zip\"\n                $zipExpandPath = \"$env:TEMP\\pricesheet\"\n                Invoke-WebRequest -Uri $downloadUrl -OutFile $zipExportPath\n                Write-Output \"Blob downloaded to $zipExportPath successfully.\"\n                Expand-Archive -LiteralPath $zipExportPath -DestinationPath $zipExpandPath -Force\n                Write-Output \"Zip expanded to $zipExpandPath successfully.\"\n                $csvFiles = Get-ChildItem -Path $zipExpandPath -Filter *.csv -Recurse\n                foreach ($csvFile in $csvFiles)\n                {\n                    $csvExportPath = $csvFile.FullName\n                    $finalCsvExportPath = \"$env:TEMP\\$($csvFile.Name)-final.csv\"\n                    Generate-Pricesheet -InputCSVPath $csvExportPath -OutputCSVPath $finalCsvExportPath -HeaderLine 1\n                }         \n                Remove-Item -Path $zipExportPath -Force\n                $now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n                Write-Output \"[$now] Removed $zipExportPath from local disk...\"                           \n            }\n            else\n            {\n                Write-Output \"Export is ready. Proceeding with CSV download...\"\n                $downloadUrl = ($downloadResult.Content | ConvertFrom-Json).properties.downloadUrl\n                $csvExportPath = \"$env:TEMP\\pricesheet-$billingPeriod-$BillingAccountID.csv\"\n                $finalCsvExportPath = \"$env:TEMP\\pricesheet-$billingPeriod-$BillingAccountID$($meterCategories.Replace(',',''))$($meterRegions.Replace(',',''))-$exportDate-final.csv\"\n                Invoke-WebRequest -Uri $downloadUrl -OutFile $csvExportPath\n                Write-Output \"Blob downloaded to $csvExportPath successfully.\"\n                Generate-Pricesheet -InputCSVPath $csvExportPath -OutputCSVPath $finalCsvExportPath -HeaderLine 3\n            }\n                    \n            $requestSuccess = $true\n        }\n        elseif ($downloadResult.StatusCode -eq 202)\n        {\n            Write-Output \"Was told to wait a bit more... $($downloadResult.Headers.RetryAfter.Delta.TotalSeconds) seconds.\"\n\n            $sleepSeconds = 60\n            if ($downloadResult.Headers.RetryAfter.Delta.TotalSeconds -gt 0)\n            {\n                $sleepSeconds = $downloadResult.Headers.RetryAfter.Delta.TotalSeconds\n            }\n        }\n        elseif ($downloadResult.StatusCode -eq 401)\n        {\n            Write-Output \"Had an authentication issue. Will login again and sleep just a couple of seconds.\"\n\n            if ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n            {\n                Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID\n            }\n            else\n            {    \n                Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment\n            }\n            \n            $sleepSeconds = 2\n        }\n        else\n        {\n            Write-Output \"Got an unexpected response code: $($downloadResult.StatusCode)\"\n        }\n    } \n    while (-not($requestSuccess) -and $tries -lt $MaxTries)\n\n    if ($tries -ge $MaxTries)\n    {\n        throw \"Couldn't complete request before the alloted number of $MaxTries retries\"\n    }\n\n    if (-not($requestSuccess))\n    {\n        throw \"Error returned by the Download PriceSheet API. Status Code: $($downloadResult.StatusCode). Message: $($downloadResult.Content)\"\n    }\n    else\n    {\n        Write-Output \"Export download processing complete.\"\n    }\n}\nelse\n{\n    if ($result.StatusCode -ne 204)\n    {\n        throw \"Error returned by the Download PriceSheet API. Status Code: $($result.StatusCode). Message: $($result.Content)\"\n    }\n    else\n    {\n        Write-Output \"Request returned 204 No Content\"\n    }\n}"
  },
  {
    "path": "runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RBACAssignmentsContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"rbacexports\"\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq \"Enabled\" }\n\n$roleAssignments = @()\n\n\"Iterating through all reachable subscriptions...\"\n\nforeach ($subscription in $subscriptions) {\n\n    Select-AzSubscription -SubscriptionId $subscription.Id -TenantId $tenantId | Out-Null\n\n    $assignments = Get-AzRoleAssignment -IncludeClassicAdministrators -ErrorAction Continue\n    \"Found $($assignments.Count) assignments for $($subscription.Name) subscription...\"\n\n    foreach ($assignment in $assignments) {\n        if ($null -eq $assignment.ObjectId -and $assignment.Scope.Contains($subscription.Id))\n        {\n            $assignmentEntry = New-Object PSObject -Property @{\n                Timestamp         = $timestamp\n                TenantGuid        = $tenantId\n                Cloud             = $cloudEnvironment\n                Model             = \"AzureClassic\"\n                PrincipalId       = $assignment.SignInName\n                Scope             = $assignment.Scope\n                RoleDefinition    = $assignment.RoleDefinitionName\n            }\n            $roleAssignments += $assignmentEntry            \n        }\n        else\n        {\n            $duplicateRoleAssignment = $roleAssignments | Where-Object { $_.PrincipalId -eq $assignment.ObjectId -and $_.Scope -eq $assignment.Scope -and $_.RoleDefinition -eq $assignment.RoleDefinitionName}\n            if (-not($duplicateRoleAssignment))\n            {\n                $assignmentEntry = New-Object PSObject -Property @{\n                    Timestamp         = $timestamp\n                    TenantGuid        = $tenantId\n                    Cloud             = $cloudEnvironment\n                    Model             = \"AzureRM\"\n                    PrincipalId       = $assignment.ObjectId\n                    Scope             = $assignment.Scope\n                    RoleDefinition    = $assignment.RoleDefinitionName\n                }\n                $roleAssignments += $assignmentEntry                            \n            }\n        }\n    }       \n}\n\n$fileDate = $datetime.ToString(\"yyyyMMdd\")\n$jsonExportPath = \"$fileDate-$tenantId-rbacassignments.json\"\n$csvExportPath = \"$fileDate-$tenantId-rbacassignments.csv\"\n\n$roleAssignments | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath\n\"Exported to JSON: $($roleAssignments.Count) lines\"\n$rbacObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json\n\"JSON Import: $($rbacObjectsJson.Count) lines\"\n$rbacObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath\n\"Export to $csvExportPath\"\n\n$csvBlobName = $csvExportPath\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\"[$now] Removed $csvExportPath from local disk...\"    \n\nRemove-Item -Path $jsonExportPath -Force\n    \n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\"[$now] Removed $jsonExportPath from local disk...\"    \n\n$roleAssignments = @()\n\n\"Getting Microsoft Entra ID roles...\"\n\n#workaround for https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/888\n$localPath = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::UserProfile)\nif (-not(get-item \"$localPath\\.graph\\\" -ErrorAction SilentlyContinue))\n{\n    New-Item -Type Directory \"$localPath\\.graph\"\n}\n\nImport-Module Microsoft.Graph.Identity.DirectoryManagement\n\nswitch ($cloudEnvironment) {\n    \"AzureUSGovernment\" {  \n        $graphEnvironment = \"USGov\"\n        break\n    }\n    \"AzureChinaCloud\" {  \n        $graphEnvironment = \"China\"\n        break\n    }\n    \"AzureGermanCloud\" {  \n        $graphEnvironment = \"Germany\"\n        break\n    }\n    Default {\n        $graphEnvironment = \"Global\"\n    }\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Microsoft Graph with $externalCredentialName external credential...\"\n    Connect-MgGraph -TenantId $externalTenantId -ClientSecretCredential $externalCredential -Environment $graphEnvironment -NoWelcome\n}\nelse\n{\n    \"Logging in to Microsoft Graph...\"\n    Connect-MgGraph -Identity -Environment $graphEnvironment -NoWelcome\n}\n\n$domainName = (Get-MgDomain | Where-Object { $_.IsVerified -and $_.IsDefault } | Select-Object -First 1).Id\n\n$roles = Get-MgDirectoryRole -ExpandProperty Members -Property DisplayName,Members\nforeach ($role in $roles)\n{\n    $roleMembers = $role.Members | Where-Object { -not($_.DeletedDateTime) }\n    foreach ($roleMember in $roleMembers)\n    {\n        $assignmentEntry = New-Object PSObject -Property @{\n            Timestamp         = $timestamp\n            TenantGuid        = $tenantId\n            Cloud             = $cloudEnvironment\n            Model             = \"AzureAD\"\n            PrincipalId       = $roleMember.Id\n            Scope             = $domainName\n            RoleDefinition    = $role.DisplayName\n        }\n        $roleAssignments += $assignmentEntry                            \n    }\n}\n\n$fileDate = $datetime.ToString(\"yyyyMMdd\")\n$jsonExportPath = \"$fileDate-$tenantId-aadrbacassignments.json\"\n$csvExportPath = \"$fileDate-$tenantId-aadrbacassignments.csv\"\n\n$roleAssignments | ConvertTo-Json -Depth 3 -Compress | Out-File $jsonExportPath\n\"Exported to JSON: $($roleAssignments.Count) lines\"\n$rbacObjectsJson = Get-Content -Path $jsonExportPath | ConvertFrom-Json\n\"JSON Import: $($rbacObjectsJson.Count) lines\"\n$rbacObjectsJson | Export-Csv -NoTypeInformation -Path $csvExportPath\n\"Export to $csvExportPath\"\n\n$csvBlobName = $csvExportPath\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force    \n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\"[$now] Removed $csvExportPath from local disk...\"    \n\nRemove-Item -Path $jsonExportPath -Force\n    \n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\"[$now] Removed $jsonExportPath from local disk...\"    \n"
  },
  {
    "path": "runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)] \n    [string] $Filter = \"serviceName eq 'Virtual Machines' and priceType eq 'Reservation'\" # e.g., serviceName eq 'Virtual Machines' and priceType eq 'Reservation' and armRegionName eq 'northeurope'\n)\n\n$ErrorActionPreference = \"Stop\"\n\nfunction Authenticate-AzureWithOption {\n    param (\n        [string] $authOption = \"ManagedIdentity\",\n        [string] $cloudEnv = \"AzureCloud\",\n        [string] $clientID \n    )\n\n    switch ($authOption) {\n        \"UserAssignedManagedIdentity\" { \n            Connect-AzAccount -Identity -EnvironmentName $cloudEnv -AccountId $clientID\n            break\n        }\n        Default { #ManagedIdentity\n            Connect-AzAccount -Identity -EnvironmentName $cloudEnv \n            break\n        }\n    }\n}\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ReservationsPriceContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"reservationspriceexports\"\n}\n\n$filterVar = Get-AutomationVariable -Name \"AzureOptimization_RetailPricesFilter\" -ErrorAction SilentlyContinue\n$currencyCode = Get-AutomationVariable -Name \"AzureOptimization_RetailPricesCurrencyCode\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment -clientID $uamiClientID\n}\nelse\n{    \n    Authenticate-AzureWithOption -authOption $authenticationOption -cloudEnv $cloudEnvironment\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\nif (-not([string]::IsNullOrEmpty($filterVar)))\n{\n    $Filter = $filterVar\n}\n\nWrite-Output \"Starting retails prices export process with $currencyCode currency code and filter: $Filter ...\"\n\n$RetailPricesApiPath = \"https://prices.azure.com/api/retail/prices?currencyCode='$currencyCode'&`$filter=$Filter\"\n\n$prices = @()\n\ndo\n{\n    $Response = Invoke-RestMethod -Method Get -Uri $RetailPricesApiPath\n    if ($Response.Items.Count -gt 0)\n    {\n        $prices += $Response.Items\n    }\n    $RetailPricesApiPath = $Response.NextPageLink\n} while ($Response.NextPageLink)\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyyMMdd\")\n\n$fileFriendlyFilter = $Filter.Replace(\" \",\"\").Replace(\"'\",\"\")\n$csvExportPath = \"reservationsprice-$timestamp-$fileFriendlyFilter.csv\"\n\n$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name)\nif ($ci.NumberFormat.NumberDecimalSeparator -ne '.')\n{\n    Write-Output \"Current culture ($($ci.Name)) does not use . as decimal separator\"    \n    $ci.NumberFormat.NumberDecimalSeparator = '.'\n    [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci\n}\n\n$prices | Export-Csv -NoTypeInformation -Path $csvExportPath\n        \nWrite-Output \"Reservations price CSV exported to $csvExportPath successfully.\"\n\n$csvBlobName = $csvExportPath\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"                    \n"
  },
  {
    "path": "runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetScope,\n\n    [Parameter(Mandatory = $false)]\n    [string] $BillingAccountID,\n\n    [Parameter(Mandatory = $false)]\n    [string] $BillingProfileID,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName,\n\n    [Parameter(Mandatory = $false)] \n    [string] $targetStartDate, # YYYY-MM-DD format\n\n    [Parameter(Mandatory = $false)] \n    [string] $targetEndDate # YYYY-MM-DD format\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_ReservationsContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"reservationsexports\"\n}\n\n$BillingAccountIDVar = Get-AutomationVariable -Name  \"AzureOptimization_BillingAccountID\" -ErrorAction SilentlyContinue\n$BillingProfileIDVar = Get-AutomationVariable -Name  \"AzureOptimization_BillingProfileID\" -ErrorAction SilentlyContinue\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n\nif ([string]::IsNullOrEmpty($BillingAccountID) -and -not([string]::IsNullOrEmpty($BillingAccountIDVar)))\n{\n    $BillingAccountID = $BillingAccountIDVar\n}\n\nif ([string]::IsNullOrEmpty($BillingProfileID) -and -not([string]::IsNullOrEmpty($BillingProfileIDVar)))\n{\n    $BillingProfileID = $BillingProfileIDVar\n}\n\n$mcaBillingAccountIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n$mcaBillingProfileIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\n# compute start+end dates\n\nif ([string]::IsNullOrEmpty($targetStartDate) -or [string]::IsNullOrEmpty($targetEndDate))\n{\n    $targetStartDate = (Get-Date).Date.AddDays($consumptionOffsetDays * -1).ToString(\"yyyy-MM-dd\")\n    $targetEndDate = $targetStartDate    \n}\n\nif (-not([string]::IsNullOrEmpty($TargetScope)))\n{\n    $scope = $TargetScope\n}\nelse\n{\n    if ([string]::IsNullOrEmpty($BillingAccountID))\n    {\n        throw \"Billing Account ID undefined. Use either the AzureOptimization_BillingAccountID variable or the BillingAccountID parameter\"\n    }\n    if ($BillingAccountID -match $mcaBillingAccountIdRegex)\n    {\n        if ([string]::IsNullOrEmpty($BillingProfileID))\n        {\n            throw \"Billing Profile ID undefined for MCA. Use either the AzureOptimization_BillingProfileID variable or the BillingProfileID parameter\"\n        }\n        if (-not($BillingProfileID -match $mcaBillingProfileIdRegex))\n        {\n            throw \"Billing Profile ID does not follow pattern for MCA: ([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n        }\n        $scope = \"/providers/Microsoft.Billing/billingaccounts/$BillingAccountID/billingProfiles/$BillingProfileID\"\n    }\n    else\n    {\n        $scope = \"/providers/Microsoft.Billing/billingaccounts/$BillingAccountID\"\n    }\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Starting reservations export process from $targetStartDate to $targetEndDate for scope $scope...\"\n\n# get reservations details\n\n$reservationsDetailsResponse = $null\n$reservationsDetails = @()\n$reservationsDetailsPath = \"$scope/reservations?api-version=2020-05-01&&refreshSummary=true\"\n\ndo\n{\n    if (-not([string]::IsNullOrEmpty($reservationsDetailsResponse.nextLink)))\n    {\n        $reservationsDetailsPath = $reservationsDetailsResponse.nextLink.Substring($reservationsDetailsResponse.nextLink.IndexOf(\"/providers/\"))\n    }\n\n    $result = Invoke-AzRestMethod -Path $reservationsDetailsPath -Method GET\n\n    if (-not($result.StatusCode -in (200, 201, 202)))\n    {\n        throw \"Error while getting reservations details: $($result.Content)\"\n    }\n\n    $reservationsDetailsResponse = $result.Content | ConvertFrom-Json\n    if ($reservationsDetailsResponse.value)\n    {\n        $reservationsDetails += $reservationsDetailsResponse.value\n    }\n}\nwhile (-not([string]::IsNullOrEmpty($reservationsDetailsResponse.nextLink)))\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Found $($reservationsDetails.Count) reservation details.\"\n\n# get reservations usage\n\n$reservationsUsage = @()\nif ($BillingAccountID -match $mcaBillingAccountIdRegex)\n{\n    $reservationsUsagePath = \"$scope/providers/Microsoft.Consumption/reservationSummaries?api-version=2023-05-01&startDate=$targetStartDate&endDate=$targetEndDate&grain=daily\"\n}\nelse\n{\n    $reservationsUsagePath = \"$scope/providers/Microsoft.Consumption/reservationSummaries?api-version=2023-05-01&`$filter=properties/UsageDate ge $targetStartDate and properties/UsageDate le $targetEndDate&grain=daily\"\n}\n\n$result = Invoke-AzRestMethod -Path $reservationsUsagePath -Method GET\n\nif (-not($result.StatusCode -in (200, 201, 202)))\n{\n    throw \"Error while getting reservations usage: $($result.Content)\"\n}\n\n$reservationsUsageResponse = $result.Content | ConvertFrom-Json\nif ($reservationsUsageResponse.value)\n{\n    $reservationsUsage += $reservationsUsageResponse.value\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Found $($reservationsUsage.Count) reservation usages.\"\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$reservations = @()\n\nforeach ($usage in $reservationsUsage)\n{\n    $reservationResourceId = \"/providers/microsoft.capacity/reservationorders/$($usage.properties.reservationOrderId)/reservations/$($usage.properties.reservationId)\"\n    $reservationDetail = $reservationsDetails | Where-Object { $_.id -eq $reservationResourceId }\n    $reservationEntry = New-Object PSObject -Property @{\n        ReservationResourceId = $reservationResourceId\n        ReservationOrderId = $usage.properties.reservationOrderId\n        ReservationId = $usage.properties.reservationId\n        DisplayName = $reservationDetail.properties.displayName\n        SKUName = $usage.properties.skuName\n        Location = $reservationDetail.location\n        ResourceType = $reservationDetail.properties.reservedResourceType\n        AppliedScopeType = $reservationDetail.properties.userFriendlyAppliedScopeType\n        Term = $reservationDetail.properties.term\n        ProvisioningState = $reservationDetail.properties.displayProvisioningState\n        RenewState = $reservationDetail.properties.userFriendlyRenewState\n        PurchaseDate = $reservationDetail.properties.purchaseDate\n        ExpiryDate = $reservationDetail.properties.expiryDate\n        Archived = $reservationDetail.properties.archived\n        ReservedHours = $usage.properties.reservedHours\n        UsedHours = $usage.properties.usedHours\n        UsageDate = $usage.properties.usageDate\n        MinUtilPercentage = $usage.properties.minUtilizationPercentage\n        AvgUtilPercentage = $usage.properties.avgUtilizationPercentage\n        MaxUtilPercentage = $usage.properties.maxUtilizationPercentage\n        PurchasedQuantity = $usage.properties.purchasedQuantity\n        RemainingQuantity = $usage.properties.remainingQuantity\n        TotalReservedQuantity = $usage.properties.totalReservedQuantity\n        UsedQuantity = $usage.properties.usedQuantity\n        UtilizedPercentage = $usage.properties.utilizedPercentage\n        UtilTrend = $reservationDetail.properties.utilization.trend\n        Util1Days = ($reservationDetail.properties.utilization.aggregates | Where-Object { $_.grain -eq 1 }).value\n        Util7Days = ($reservationDetail.properties.utilization.aggregates | Where-Object { $_.grain -eq 7 }).value\n        Util30Days = ($reservationDetail.properties.utilization.aggregates | Where-Object { $_.grain -eq 30 }).value\n        Scope = $scope\n        TenantGuid = $tenantId\n        Cloud = $cloudEnvironment\n        CollectedDate = $timestamp\n        Timestamp = $timestamp\n    }\n    $reservations += $reservationEntry\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Generated $($reservations.Count) entries...\"\n\nif ($BillingAccountID -match $mcaBillingAccountIdRegex)\n{\n    $csvExportPath = \"$targetStartDate-$BillingProfileID.csv\"   \n}\nelse\n{\n    $csvExportPath = \"$targetStartDate-$BillingAccountID-$($scope.Split('/')[-1]).csv\"\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploading CSV to Storage\"\n\n$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name)\nif ($ci.NumberFormat.NumberDecimalSeparator -ne '.')\n{\n    Write-Output \"Current culture ($($ci.Name)) does not use . as decimal separator\"    \n    $ci.NumberFormat.NumberDecimalSeparator = '.'\n    [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci\n}\n\n$reservations | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n    \n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [string] $TargetScope,\n\n    [Parameter(Mandatory = $false)]\n    [string] $BillingAccountID,\n\n    [Parameter(Mandatory = $false)]\n    [string] $BillingProfileID,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCloudEnvironment,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalTenantId,\n\n    [Parameter(Mandatory = $false)]\n    [string] $externalCredentialName\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkEnv = Get-AutomationVariable -Name \"AzureOptimization_StorageSinkEnvironment\" -ErrorAction SilentlyContinue\nif (-not($storageAccountSinkEnv))\n{\n    $storageAccountSinkEnv = $cloudEnvironment    \n}\n$storageAccountSinkKeyCred = Get-AutomationPSCredential -Name \"AzureOptimization_StorageSinkKey\" -ErrorAction SilentlyContinue\n$storageAccountSinkKey = $null\nif ($storageAccountSinkKeyCred)\n{\n    $storageAccountSink = $storageAccountSinkKeyCred.UserName\n    $storageAccountSinkKey = $storageAccountSinkKeyCred.GetNetworkCredential().Password\n}\n\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_SavingsPlansContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer))\n{\n    $storageAccountSinkContainer = \"savingsplansexports\"\n}\n\n$BillingAccountIDVar = Get-AutomationVariable -Name  \"AzureOptimization_BillingAccountID\" -ErrorAction SilentlyContinue\n$BillingProfileIDVar = Get-AutomationVariable -Name  \"AzureOptimization_BillingProfileID\" -ErrorAction SilentlyContinue\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    $externalCredential = Get-AutomationPSCredential -Name $externalCredentialName\n}\n\nif ([string]::IsNullOrEmpty($BillingAccountID) -and -not([string]::IsNullOrEmpty($BillingAccountIDVar)))\n{\n    $BillingAccountID = $BillingAccountIDVar\n}\n\nif ([string]::IsNullOrEmpty($BillingProfileID) -and -not([string]::IsNullOrEmpty($BillingProfileIDVar)))\n{\n    $BillingProfileID = $BillingProfileIDVar\n}\n\n$mcaBillingAccountIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+):([A-Za-z0-9]+(-[A-Za-z0-9]+)+)_[0-9]{4}-[0-9]{2}-[0-9]{2}\"\n$mcaBillingProfileIdRegex = \"([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nif (-not($storageAccountSinkKey))\n{\n    Write-Output \"Getting Storage Account context with login\"\n    Select-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n    $saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n}\nelse\n{\n    Write-Output \"Getting Storage Account context with key\"\n    $saCtx = New-AzStorageContext -StorageAccountName $storageAccountSink -StorageAccountKey $storageAccountSinkKey -Environment $storageAccountSinkEnv\n}\n\nif (-not([string]::IsNullOrEmpty($externalCredentialName)))\n{\n    \"Logging in to Azure with $externalCredentialName external credential...\"\n    Connect-AzAccount -ServicePrincipal -EnvironmentName $externalCloudEnvironment -Tenant $externalTenantId -Credential $externalCredential \n    $cloudEnvironment = $externalCloudEnvironment   \n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\nif (-not([string]::IsNullOrEmpty($TargetScope)))\n{\n    $scope = $TargetScope\n}\nelse\n{\n    if ([string]::IsNullOrEmpty($BillingAccountID))\n    {\n        throw \"Billing Account ID undefined. Use either the AzureOptimization_BillingAccountID variable or the BillingAccountID parameter\"\n    }\n    if ($BillingAccountID -match $mcaBillingAccountIdRegex)\n    {\n        if ([string]::IsNullOrEmpty($BillingProfileID))\n        {\n            throw \"Billing Profile ID undefined for MCA. Use either the AzureOptimization_BillingProfileID variable or the BillingProfileID parameter\"\n        }\n        if (-not($BillingProfileID -match $mcaBillingProfileIdRegex))\n        {\n            throw \"Billing Profile ID does not follow pattern for MCA: ([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\"\n        }\n        #$scope = \"/providers/Microsoft.BillingBenefits\"\n        $scope = \"/providers/Microsoft.Billing/billingaccounts/$BillingAccountID\"\n    }\n    else\n    {\n        $scope = \"/providers/Microsoft.Billing/billingaccounts/$BillingAccountID\"\n    }\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Starting savings plans export process for scope $scope...\"\n\n$savingsPlansUsage = @()\nif ($BillingAccountID -match $mcaBillingAccountIdRegex)\n{\n    #$savingsPlansUsagePath = \"$scope/savingsPlans?api-version=2022-11-01&refreshsummary=true&take=100\"\n    $savingsPlansUsagePath = \"$scope/savingsPlans?api-version=2022-10-01-privatepreview&refreshsummary=true&take=100&`$filter=(properties/billingProfileId eq '/providers/Microsoft.Billing/billingAccounts/$BillingAccountID/billingProfiles/$BillingProfileID')\"\n}\nelse\n{\n    $savingsPlansUsagePath = \"$scope/savingsPlans?api-version=2020-12-15-privatepreview&refreshsummary=true&take=100\"\n}\n\n$result = Invoke-AzRestMethod -Path $savingsPlansUsagePath -Method GET\n\nif (-not($result.StatusCode -in (200, 201, 202)))\n{\n    throw \"Error while getting savings plans usage: $($result.Content)\"\n}\n\n$savingsPlansUsageResponse = $result.Content | ConvertFrom-Json\nif ($savingsPlansUsageResponse.value)\n{\n    $savingsPlansUsage += $savingsPlansUsageResponse.value\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Found $($savingsPlansUsage.Count) savings plans usages.\"\n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$savingsPlans = @()\n\nforeach ($usage in $savingsPlansUsage)\n{\n    $savingsPlanEntry = New-Object PSObject -Property @{\n        SavingsPlanResourceId = $usage.id\n        SavingsPlanOrderId = $usage.id.Substring(0,$usage.id.IndexOf(\"/savingsPlans/\"))\n        SavingsPlanId = $usage.id.Split(\"/\")[-1]\n        DisplayName = $usage.properties.displayName\n        SKUName = $usage.sku.name\n        Term = $usage.properties.term\n        ProvisioningState = $usage.properties.displayProvisioningState\n        AppliedScopeType = $usage.properties.userFriendlyAppliedScopeType\n        RenewState = $usage.properties.renew\n        PurchaseDate = $usage.properties.purchaseDateTime\n        BenefitStart = $usage.properties.benefitStartTime\n        ExpiryDate = $usage.properties.expiryDateTime\n        EffectiveDate = $usage.properties.effectiveDateTime\n        BillingScopeId = $usage.properties.billingScopeId\n        BillingAccountId = $usage.properties.billingAccountId\n        BillingProfileId = $usage.properties.billingProfileId\n        BillingPlan = $usage.properties.billingProfileId\n        CommitmentGrain = $usage.properties.commitment.grain\n        CommitmentCurrencyCode = $usage.properties.commitment.currencyCode\n        CommitmentAmount = $usage.properties.commitment.amount\n        UtilTrend = $usage.properties.utilization.trend\n        Util1Days = ($usage.properties.utilization.aggregates | Where-Object { $_.grain -eq 1 }).value\n        Util7Days = ($usage.properties.utilization.aggregates | Where-Object { $_.grain -eq 7 }).value\n        Util30Days = ($usage.properties.utilization.aggregates | Where-Object { $_.grain -eq 30 }).value\n        Scope = $scope\n        TenantGuid = $tenantId\n        Cloud = $cloudEnvironment\n        CollectedDate = $timestamp\n        Timestamp = $timestamp\n    }\n    $savingsPlans += $savingsPlanEntry\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Generated $($savingsPlans.Count) entries...\"\n\n$targetDate = $datetime.ToString(\"yyyy-MM-dd\")\n\nif ($BillingAccountID -match $mcaBillingAccountIdRegex)\n{\n    $csvExportPath = \"$targetDate-$BillingProfileID.csv\"   \n}\nelse\n{\n    $csvExportPath = \"$targetDate-$BillingAccountID.csv\"\n}\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploading CSV to Storage\"\n\n$ci = [CultureInfo]::new([System.Threading.Thread]::CurrentThread.CurrentCulture.Name)\nif ($ci.NumberFormat.NumberDecimalSeparator -ne '.')\n{\n    Write-Output \"Current culture ($($ci.Name)) does not use . as decimal separator\"    \n    $ci.NumberFormat.NumberDecimalSeparator = '.'\n    [System.Threading.Thread]::CurrentThread.CurrentCulture = $ci\n}\n\n$savingsPlans | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n    \n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $csvBlobName to Blob Storage...\"\n\nRemove-Item -Path $csvExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $csvExportPath from local disk...\"    "
  },
  {
    "path": "runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1",
    "content": "param(\n    [Parameter(Mandatory = $true)]\n    [string] $StorageSinkContainer\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$sharedKey = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceKey\"\n$LogAnalyticsChunkSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsChunkSize\" -ErrorAction SilentlyContinue)\nif (-not($LogAnalyticsChunkSize -gt 0))\n{\n    $LogAnalyticsChunkSize = 6000\n}\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = $StorageSinkContainer\n$StorageBlobsPageSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_StorageBlobsPageSize\" -ErrorAction SilentlyContinue)\nif (-not($StorageBlobsPageSize -gt 0))\n{\n    $StorageBlobsPageSize = 1000\n}\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n#region Functions\n\n# Function to create the authorization signature\nFunction Build-OMSSignature ($workspaceId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) {\n    $xHeaders = \"x-ms-date:\" + $date\n    $stringToHash = $method + \"`n\" + $contentLength + \"`n\" + $contentType + \"`n\" + $xHeaders + \"`n\" + $resource\n    $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)\n    $keyBytes = [Convert]::FromBase64String($sharedKey)\n    $sha256 = New-Object System.Security.Cryptography.HMACSHA256\n    $sha256.Key = $keyBytes\n    $calculatedHash = $sha256.ComputeHash($bytesToHash)\n    $encodedHash = [Convert]::ToBase64String($calculatedHash)\n    $authorization = 'SharedKey {0}:{1}' -f $workspaceId, $encodedHash\n    return $authorization\n}\n\n# Function to create and post the request\nFunction Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField, $AzureEnvironment) {\n    $method = \"POST\"\n    $contentType = \"application/json\"\n    $resource = \"/api/logs\"\n    $rfc1123date = [DateTime]::UtcNow.ToString(\"r\")\n    $contentLength = $body.Length\n    $signature = Build-OMSSignature `\n        -workspaceId $workspaceId `\n        -sharedKey $sharedKey `\n        -date $rfc1123date `\n        -contentLength $contentLength `\n        -method $method `\n        -contentType $contentType `\n        -resource $resource\n    \n    $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.com\" + $resource + \"?api-version=2016-04-01\"\n    if ($AzureEnvironment -eq \"AzureChinaCloud\")\n    {\n        $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.cn\" + $resource + \"?api-version=2016-04-01\"\n    }\n    if ($AzureEnvironment -eq \"AzureUSGovernment\")\n    {\n        $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.us\" + $resource + \"?api-version=2016-04-01\"\n    }\n    if ($AzureEnvironment -eq \"AzureGermanCloud\")\n    {\n        throw \"Azure Germany isn't suported for the Log Analytics Data Collector API\"\n    }\n\n    $OMSheaders = @{\n        \"Authorization\"        = $signature;\n        \"Log-Type\"             = $logType;\n        \"x-ms-date\"            = $rfc1123date;\n        \"time-generated-field\" = $TimeStampField;\n    }\n\n    Try {\n\n        $response = Invoke-WebRequest -Uri $uri -Method POST  -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000\n    }\n    catch {\n        if ($_.Exception.Response.StatusCode.Value__ -eq 401) {            \n            \"REAUTHENTICATING\"\n\n            $response = Invoke-WebRequest -Uri $uri -Method POST  -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000\n        }\n        else\n        {\n            return $_.Exception.Response.StatusCode.Value__\n        }\n    }\n\n    return $response.StatusCode    \n}\n#endregion Functions\n\n# get reference to storage sink\nWrite-Output \"Getting blobs list from $storageAccountSink storage account ($storageAccountSinkContainer container)...\"\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\n$allblobs = @()\n\n$continuationToken = $null\ndo\n{\n    $blobs = Get-AzStorageBlob -Container $storageAccountSinkContainer -MaxCount $StorageBlobsPageSize -ContinuationToken $continuationToken -Context $sa.Context | Sort-Object -Property LastModified\n    if ($blobs.Count -le 0) { break }\n    $allblobs += $blobs\n    $continuationToken = $blobs[$blobs.Count -1].ContinuationToken;\n}\nWhile ($null -ne $continuationToken)\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE StorageContainerName = '$storageAccountSinkContainer'\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null\n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$Conn.Close()    \n$Conn.Dispose()            \n\nif ($controlRows.Count -eq 0 -or -not($controlRows[0].LastProcessedDateTime))\n{\n    throw \"Could not find a valid ingestion control row for $storageAccountSinkContainer\"\n}\n\n$controlRow = $controlRows[0]\n$lastProcessedLine = $controlRow.LastProcessedLine\n$lastProcessedDateTime = $controlRow.LastProcessedDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n$LogAnalyticsSuffix = $controlRow.LogAnalyticsSuffix\n$logname = $lognamePrefix + $LogAnalyticsSuffix\n\nWrite-Output \"Processing blobs modified after $lastProcessedDateTime (line $lastProcessedLine) and ingesting them into the $($logname)_CL table...\"\n\n$newProcessedTime = $null\n\n$unprocessedBlobs = @()\n\nforeach ($blob in $allblobs) {\n\t$blobLastModified = $blob.LastModified.UtcDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    if ($lastProcessedDateTime -lt $blobLastModified -or `\n        ($lastProcessedDateTime -eq $blobLastModified -and $lastProcessedLine -gt 0)) {\n\t\tWrite-Output \"$($blob.Name) found (modified on $blobLastModified)\"\n        $unprocessedBlobs += $blob\n    }\n}\n\n$unprocessedBlobs = $unprocessedBlobs | Sort-Object -Property LastModified\n\nWrite-Output \"Found $($unprocessedBlobs.Count) new blobs to process...\"\n\nforeach ($blob in $unprocessedBlobs) {\n    $newProcessedTime = $blob.LastModified.UtcDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    Write-Output \"About to process $($blob.Name)...\"\n    $blobFilePath = \"$env:TEMP\\$($blob.Name)\"\n    Get-AzStorageBlobContent -CloudBlob $blob.ICloudBlob -Context $sa.Context -Force -Destination $blobFilePath | Out-Null\n\n    $r = [IO.File]::OpenText($blobFilePath)\n\n    $linesProcessed = 0\n    $lineCounter = 0\n    $chunkLines = @()\n\n    while ($r.Peek() -ge 0) \n    {\n        $line = $r.ReadLine()\n        if ($lineCounter -eq 0)\n        {\n            $header = $line\n            $chunkLines += $line\n        }\n        else\n        {\n            $linesProcessed++    \n        }\n        if ($lastProcessedLine -lt $linesProcessed -and $lineCounter -gt 0)\n        {\n            $chunkLines += $line\n        }\n        if (($lineCounter -eq $LogAnalyticsChunkSize -or $r.Peek() -lt 0) -and $linesProcessed -gt 0)\n        {\n            $csvObject = $chunkLines | ConvertFrom-Csv\n            $jsonObject = ConvertTo-Json -InputObject $csvObject\n\n            $res = Post-OMSData -workspaceId $workspaceId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($jsonObject)) -logType $logname -TimeStampField \"Timestamp\" -AzureEnvironment $cloudEnvironment\n            if ($res -ge 200 -and $res -lt 300) \n            {\n                Write-Output \"Succesfully uploaded $lineCounter $LogAnalyticsSuffix rows to Log Analytics\"    \n                if ($r.Peek() -lt 0) {\n                    $lastProcessedLine = -1    \n                }\n                else {\n                    $lastProcessedLine = $linesProcessed - 1   \n                }\n                \n                $updatedLastProcessedLine = $lastProcessedLine\n                $updatedLastProcessedDateTime = $lastProcessedDateTime\n                if ($r.Peek() -lt 0) {\n                    $updatedLastProcessedDateTime = $newProcessedTime\n                }\n                $lastProcessedDateTime = $updatedLastProcessedDateTime\n                Write-Output \"Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine\"\n                $sqlStatement = \"UPDATE [$LogAnalyticsIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'\"\n                $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n                $Conn.Open() \n                $Cmd=new-object system.Data.SqlClient.SqlCommand\n                $Cmd.Connection = $Conn\n                $Cmd.CommandText = $sqlStatement\n                $Cmd.CommandTimeout=120 \n                $Cmd.ExecuteReader()\n                $Conn.Close()    \n                $Conn.Dispose()            \n            }\n            else \n            {\n                Write-Warning \"Failed to upload $lineCounter $LogAnalyticsSuffix rows. Error code: $res\"\n                $r.Dispose()\n                Remove-Item -Path $blobFilePath -Force\n                throw\n            }\n\n            $chunkLines = @()\n            $chunkLines += $header\n            $lineCounter = 1\n        }\n        else\n        {\n            $lineCounter++\n        }        \n    }\n    $r.Dispose()\n\n    if ($linesProcessed -eq 0)\n    {\n        Write-Output \"No rows found\"\n        $updatedLastProcessedLine = -1 \n        $updatedLastProcessedDateTime = $newProcessedTime\n        Write-Output \"Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine\"\n        $sqlStatement = \"UPDATE [$LogAnalyticsIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'\"\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandText = $sqlStatement\n        $Cmd.CommandTimeout=120 \n        $Cmd.ExecuteReader()\n        $Conn.Close()    \n        $Conn.Dispose()            \n    }\n    else\n    {\n        Write-Output \"Processed $linesProcessed row(s) in total.\"  \n    }\n    \n    Remove-Item -Path $blobFilePath -Force\n}\n\nWrite-Output \"DONE\""
  },
  {
    "path": "runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$RecommendationsMaxAge = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsMaxAgeInDays\" -ErrorAction SilentlyContinue)\nif (-not($RecommendationsMaxAge -gt 0))\n{\n    $RecommendationsMaxAge = 365\n}\n\n$recommendationsTable = \"Recommendations\"\n\n$tries = 0\n$connectionSuccess = $false\n\nWrite-Output \"Cleaning up recommendations older than $RecommendationsMaxAge days...\"\n\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = 0\n        $Cmd.CommandText = \"DELETE FROM [dbo].[$recommendationsTable] WHERE GeneratedDate < GETDATE()-$RecommendationsMaxAge\"\n        $DeletedRows = $Cmd.ExecuteNonQuery()            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }\n    finally {\n        $Conn.Close()    \n        $Conn.Dispose()            \n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\nWrite-Output \"Cleaned up $DeletedRows recommendations.\""
  },
  {
    "path": "runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$sharedKey = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceKey\"\n$LogAnalyticsChunkSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsChunkSize\" -ErrorAction SilentlyContinue)\nif (-not($LogAnalyticsChunkSize -gt 0))\n{\n    $LogAnalyticsChunkSize = 6000\n}\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n$StorageBlobsPageSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_StorageBlobsPageSize\" -ErrorAction SilentlyContinue)\nif (-not($StorageBlobsPageSize -gt 0))\n{\n    $StorageBlobsPageSize = 1000\n}\n\n#region Functions\n\n# Function to create the authorization signature\nFunction Build-OMSSignature ($workspaceId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) {\n    $xHeaders = \"x-ms-date:\" + $date\n    $stringToHash = $method + \"`n\" + $contentLength + \"`n\" + $contentType + \"`n\" + $xHeaders + \"`n\" + $resource\n    $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)\n    $keyBytes = [Convert]::FromBase64String($sharedKey)\n    $sha256 = New-Object System.Security.Cryptography.HMACSHA256\n    $sha256.Key = $keyBytes\n    $calculatedHash = $sha256.ComputeHash($bytesToHash)\n    $encodedHash = [Convert]::ToBase64String($calculatedHash)\n    $authorization = 'SharedKey {0}:{1}' -f $workspaceId, $encodedHash\n    return $authorization\n}\n\n# Function to create and post the request\nFunction Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField, $AzureEnvironment) {\n    $method = \"POST\"\n    $contentType = \"application/json\"\n    $resource = \"/api/logs\"\n    $rfc1123date = [DateTime]::UtcNow.ToString(\"r\")\n    $contentLength = $body.Length\n    $signature = Build-OMSSignature `\n        -workspaceId $workspaceId `\n        -sharedKey $sharedKey `\n        -date $rfc1123date `\n        -contentLength $contentLength `\n        -method $method `\n        -contentType $contentType `\n        -resource $resource\n    \n    $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.com\" + $resource + \"?api-version=2016-04-01\"\n    if ($AzureEnvironment -eq \"AzureChinaCloud\")\n    {\n        $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.cn\" + $resource + \"?api-version=2016-04-01\"\n    }\n    if ($AzureEnvironment -eq \"AzureUSGovernment\")\n    {\n        $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.us\" + $resource + \"?api-version=2016-04-01\"\n    }\n    if ($AzureEnvironment -eq \"AzureGermanCloud\")\n    {\n        throw \"Azure Germany isn't suported for the Log Analytics Data Collector API\"\n    }\n\n    $OMSheaders = @{\n        \"Authorization\"        = $signature;\n        \"Log-Type\"             = $logType;\n        \"x-ms-date\"            = $rfc1123date;\n        \"time-generated-field\" = $TimeStampField;\n    }\n\n    Try {\n\n        $response = Invoke-WebRequest -Uri $uri -Method POST  -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000\n    }\n    catch {\n        if ($_.Exception.Response.StatusCode.Value__ -eq 401) {            \n            \"REAUTHENTICATING\"\n\n            $response = Invoke-WebRequest -Uri $uri -Method POST  -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000\n        }\n        else\n        {\n            return $_.Exception.Response.StatusCode.Value__\n        }\n    }\n\n    return $response.StatusCode    \n}\n#endregion Functions\n\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n# get reference to storage sink\nWrite-Output \"Getting reference to $storageAccountSink storage account (recommendations exports sink)\"\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n\n$allblobs = @()\n\nWrite-Output \"Getting blobs list...\"\n$continuationToken = $null\ndo\n{\n    $blobs = Get-AzStorageBlob -Container $storageAccountSinkContainer -MaxCount $StorageBlobsPageSize -ContinuationToken $continuationToken -Context $saCtx | Sort-Object -Property LastModified\n    if ($blobs.Count -le 0) { break }\n    $allblobs += $blobs\n    $continuationToken = $blobs[$blobs.Count -1].ContinuationToken;\n}\nWhile ($null -ne $continuationToken)\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE StorageContainerName = '$storageAccountSinkContainer'\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null\n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$Conn.Close()    \n$Conn.Dispose()            \n\nif ($controlRows.Count -eq 0 -or -not($controlRows[0].LastProcessedDateTime))\n{\n    throw \"Could not find a valid ingestion control row for $storageAccountSinkContainer\"\n}\n\n$controlRow = $controlRows[0]\n$lastProcessedLine = $controlRow.LastProcessedLine\n$lastProcessedDateTime = $controlRow.LastProcessedDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n$LogAnalyticsSuffix = $controlRow.LogAnalyticsSuffix\n$logname = $lognamePrefix + $LogAnalyticsSuffix\n\nWrite-Output \"Processing blobs modified after $lastProcessedDateTime (line $lastProcessedLine) and ingesting them into the $($logname)_CL table...\"\n\n$newProcessedTime = $null\n\n$unprocessedBlobs = @()\n\nforeach ($blob in $allblobs) {\n    $blobLastModified = $blob.LastModified.UtcDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    if ($lastProcessedDateTime -lt $blobLastModified -or `\n        ($lastProcessedDateTime -eq $blobLastModified -and $lastProcessedLine -gt 0)) {\n        Write-Output \"$($blob.Name) found (modified on $blobLastModified)\"\n        $unprocessedBlobs += $blob\n    }\n}\n\n$unprocessedBlobs = $unprocessedBlobs | Sort-Object -Property LastModified\n\nWrite-Output \"Found $($unprocessedBlobs.Count) new blobs to process...\"\n\nforeach ($blob in $unprocessedBlobs) {\n    $newProcessedTime = $blob.LastModified.UtcDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    Write-Output \"About to process $($blob.Name)...\"\n    Get-AzStorageBlobContent -CloudBlob $blob.ICloudBlob -Context $saCtx -Force\n    $jsonObject = Get-Content -Path $blob.Name | ConvertFrom-Json\n    Write-Output \"Blob contains $($jsonObject.Count) results...\"\n\n    if ($null -eq $jsonObject)\n    {\n        $recCount = 0\n    }\n    elseif ($null -eq $jsonObject.Count)\n    {\n        $recCount = 1\n    }\n    else\n    {\n        $recCount = $jsonObject.Count    \n    }\n\n    $linesProcessed = 0\n    $jsonObjectSplitted = @()\n\n    if ($recCount -gt 1)\n    {\n        for ($i = 0; $i -lt $recCount; $i += $LogAnalyticsChunkSize) {\n            $jsonObjectSplitted += , @($jsonObject[$i..($i + ($LogAnalyticsChunkSize - 1))]);\n        }\n    }\n    else\n    {\n        $jsonObjectArray = @()\n        $jsonObjectArray += $jsonObject\n        $jsonObjectSplitted += , $jsonObjectArray   \n    }\n    \n    for ($j = 0; $j -lt $jsonObjectSplitted.Count; $j++)\n    {\n        if ($jsonObjectSplitted[$j])\n        {\n            $currentObjectLines = $jsonObjectSplitted[$j].Count\n            if ($lastProcessedLine -lt $linesProcessed)\n            {\n                for ($i = 0; $i -lt $jsonObjectSplitted[$j].Count; $i++)\n                {\n                    $jsonObjectSplitted[$j][$i].RecommendationDescription = $jsonObjectSplitted[$j][$i].RecommendationDescription.Replace(\"'\", \"\")\n                    $jsonObjectSplitted[$j][$i].RecommendationAction = $jsonObjectSplitted[$j][$i].RecommendationAction.Replace(\"'\", \"\")            \n                    $jsonObjectSplitted[$j][$i].AdditionalInfo = $jsonObjectSplitted[$j][$i].AdditionalInfo | ConvertTo-Json -Compress\n                    $jsonObjectSplitted[$j][$i].Tags = $jsonObjectSplitted[$j][$i].Tags | ConvertTo-Json -Compress\n                }\n                    \n                $jsonObject = ConvertTo-Json -InputObject $jsonObjectSplitted[$j]                \n                $res = Post-OMSData -workspaceId $workspaceId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($jsonObject)) -logType $logname -TimeStampField \"Timestamp\" -AzureEnvironment $cloudEnvironment\n                If ($res -ge 200 -and $res -lt 300) {\n                    Write-Output \"Succesfully uploaded $currentObjectLines $LogAnalyticsSuffix rows to Log Analytics\"    \n                    $linesProcessed += $currentObjectLines\n                    if ($j -eq ($jsonObjectSplitted.Count - 1)) {\n                        $lastProcessedLine = -1    \n                    }\n                    else {\n                        $lastProcessedLine = $linesProcessed - 1   \n                    }\n                    \n                    $updatedLastProcessedLine = $lastProcessedLine\n                    $updatedLastProcessedDateTime = $lastProcessedDateTime\n                    if ($j -eq ($jsonObjectSplitted.Count - 1)) {\n                        $updatedLastProcessedDateTime = $newProcessedTime\n                    }\n                    $lastProcessedDateTime = $updatedLastProcessedDateTime\n                    Write-Output \"Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine\"\n                    $sqlStatement = \"UPDATE [$LogAnalyticsIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'\"\n                    $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n                    $Conn.Open() \n                    $Cmd=new-object system.Data.SqlClient.SqlCommand\n                    $Cmd.Connection = $Conn\n                    $Cmd.CommandText = $sqlStatement\n                    $Cmd.CommandTimeout=120 \n                    $Cmd.ExecuteReader()\n                    $Conn.Close()    \n                    $Conn.Dispose()            \n                }\n                Else {\n                    $linesProcessed += $currentObjectLines\n                    Write-Warning \"Failed to upload $currentObjectLines $LogAnalyticsSuffix rows. Error code: $res\"\n                    throw\n                }\n            }\n            else\n            {\n                $linesProcessed += $currentObjectLines  \n            }        \n        }\n    }\n\n    Remove-Item -Path $blob.Name -Force\n}\n\nWrite-Output \"DONE\""
  },
  {
    "path": "runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$ChunkSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_SQLServerInsertSize\" -ErrorAction SilentlyContinue)\nif (-not($ChunkSize -gt 0))\n{\n    $ChunkSize = 900\n}\n$SqlTimeout = 120\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n$StorageBlobsPageSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_StorageBlobsPageSize\" -ErrorAction SilentlyContinue)\nif (-not($StorageBlobsPageSize -gt 0))\n{\n    $StorageBlobsPageSize = 1000\n}\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n# get reference to storage sink\nWrite-Output \"Getting reference to $storageAccountSink storage account (recommendations exports sink)\"\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n\n$allblobs = @()\n\nWrite-Output \"Getting blobs list...\"\n$continuationToken = $null\ndo\n{\n    $blobs = Get-AzStorageBlob -Container $storageAccountSinkContainer -MaxCount $StorageBlobsPageSize -ContinuationToken $continuationToken -Context $saCtx | Sort-Object -Property LastModified\n    if ($blobs.Count -le 0) { break }\n    $allblobs += $blobs\n    $continuationToken = $blobs[$blobs.Count -1].ContinuationToken;\n}\nWhile ($null -ne $continuationToken)\n\n$SqlServerIngestControlTable = \"SqlServerIngestControl\"\n$recommendationsTable = \"Recommendations\"\n\n$tries = 0\n$connectionSuccess = $false\n\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$SqlServerIngestControlTable] WHERE StorageContainerName = '$storageAccountSinkContainer' and SqlTableName = '$recommendationsTable'\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\nif ($controlRows.Count -eq 0)\n{\n    throw \"Could not find a control row for $storageAccountSinkContainer container and $recommendationsTable table.\"\n}\n\n$controlRow = $controlRows[0]    \n$lastProcessedLine = $controlRow.LastProcessedLine\n$lastProcessedDateTime = $controlRow.LastProcessedDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n\n$Conn.Close()    \n$Conn.Dispose()            \n\nWrite-Output \"Processing blobs modified after $lastProcessedDateTime (line $lastProcessedLine) and ingesting them into the Recommendations SQL table...\"\n\n$newProcessedTime = $null\n\n$unprocessedBlobs = @()\n\nforeach ($blob in $allblobs) {\n    $blobLastModified = $blob.LastModified.UtcDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    if ($lastProcessedDateTime -lt $blobLastModified -or `\n        ($lastProcessedDateTime -eq $blobLastModified -and $lastProcessedLine -gt 0)) {\n        Write-Output \"$($blob.Name) found (modified on $blobLastModified)\"\n        $unprocessedBlobs += $blob\n    }\n}\n\n$unprocessedBlobs = $unprocessedBlobs | Sort-Object -Property LastModified\n\nWrite-Output \"Found $($unprocessedBlobs.Count) new blobs to process...\"\n\nforeach ($blob in $unprocessedBlobs) {\n    $newProcessedTime = $blob.LastModified.UtcDateTime.ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\n    Write-Output \"About to process $($blob.Name)...\"\n    Get-AzStorageBlobContent -CloudBlob $blob.ICloudBlob -Context $saCtx -Force\n    $jsonObject = Get-Content -Path $blob.Name | ConvertFrom-Json\n    Write-Output \"Blob contains $($jsonObject.Count) results...\"\n\n    if ($null -eq $jsonObject)\n    {\n        $recCount = 0\n    }\n    elseif ($null -eq $jsonObject.Count)\n    {\n        $recCount = 1\n    }\n    else\n    {\n        $recCount = $jsonObject.Count    \n    }\n\n    $linesProcessed = 0\n    $jsonObjectSplitted = @()\n\n    if ($recCount -gt 1)\n    {\n        for ($i = 0; $i -lt $recCount; $i += $ChunkSize) {\n            $jsonObjectSplitted += , @($jsonObject[$i..($i + ($ChunkSize - 1))]);\n        }\n    }\n    else\n    {\n        $jsonObjectArray = @()\n        $jsonObjectArray += $jsonObject\n        $jsonObjectSplitted += , $jsonObjectArray   \n    }\n    \n    for ($j = 0; $j -lt $jsonObjectSplitted.Count; $j++)\n    {\n        if ($jsonObjectSplitted[$j])\n        {\n            $currentObjectLines = $jsonObjectSplitted[$j].Count\n            if ($lastProcessedLine -lt $linesProcessed)\n            {\n                $sqlStatement = \"INSERT INTO [$recommendationsTable]\"\n                $sqlStatement += \" (RecommendationId, GeneratedDate, Cloud, Category, ImpactedArea, Impact, RecommendationType, RecommendationSubType,\"\n                $sqlStatement += \" RecommendationSubTypeId, RecommendationDescription, RecommendationAction, InstanceId, InstanceName, AdditionalInfo,\"\n                $sqlStatement += \" ResourceGroup, SubscriptionGuid, SubscriptionName, TenantGuid, FitScore, Tags, DetailsUrl) VALUES\"\n                for ($i = 0; $i -lt $jsonObjectSplitted[$j].Count; $i++)\n                {\n                    $jsonObjectSplitted[$j][$i].RecommendationDescription = $jsonObjectSplitted[$j][$i].RecommendationDescription.Replace(\"'\", \"\")\n                    $jsonObjectSplitted[$j][$i].RecommendationAction = $jsonObjectSplitted[$j][$i].RecommendationAction.Replace(\"'\", \"\")\n                    if ($null -ne $jsonObjectSplitted[$j][$i].InstanceName)\n                    {\n                        $jsonObjectSplitted[$j][$i].InstanceName = $jsonObjectSplitted[$j][$i].InstanceName.Replace(\"'\", \"\")\n                    }            \n                    $additionalInfoString = $jsonObjectSplitted[$j][$i].AdditionalInfo | ConvertTo-Json -Compress\n                    $tagsString = $jsonObjectSplitted[$j][$i].Tags | ConvertTo-Json -Compress\n                    $subscriptionGuid = \"NULL\"\n                    if ($jsonObjectSplitted[$j][$i].SubscriptionGuid)\n                    {\n                        $subscriptionGuid = \"'$($jsonObjectSplitted[$j][$i].SubscriptionGuid)'\"\n                    }\n                    $subscriptionName = \"NULL\"\n                    if ($jsonObjectSplitted[$j][$i].SubscriptionName)\n                    {\n                        $subscriptionName = $jsonObjectSplitted[$j][$i].SubscriptionName.Replace(\"'\", \"\")\n                        $subscriptionName = \"'$subscriptionName'\"\n                    }\n                    $resourceGroup = \"NULL\"\n                    if ($jsonObjectSplitted[$j][$i].ResourceGroup)\n                    {\n                        $resourceGroup = \"'$($jsonObjectSplitted[$j][$i].ResourceGroup)'\"\n                    }\n                    $sqlStatement += \" (NEWID(), CONVERT(DATETIME, '$($jsonObjectSplitted[$j][$i].Timestamp)'), '$($jsonObjectSplitted[$j][$i].Cloud)'\"\n                    $sqlStatement += \", '$($jsonObjectSplitted[$j][$i].Category)', '$($jsonObjectSplitted[$j][$i].ImpactedArea)'\"\n                    $sqlStatement += \", '$($jsonObjectSplitted[$j][$i].Impact)', '$($jsonObjectSplitted[$j][$i].RecommendationType)'\"\n                    $sqlStatement += \", '$($jsonObjectSplitted[$j][$i].RecommendationSubType)', '$($jsonObjectSplitted[$j][$i].RecommendationSubTypeId)'\"\n                    $sqlStatement += \", '$($jsonObjectSplitted[$j][$i].RecommendationDescription)', '$($jsonObjectSplitted[$j][$i].RecommendationAction)'\"\n                    $sqlStatement += \", '$($jsonObjectSplitted[$j][$i].InstanceId)', '$($jsonObjectSplitted[$j][$i].InstanceName)', '$additionalInfoString'\"\n                    $sqlStatement += \", $resourceGroup, $subscriptionGuid, $subscriptionName, '$($jsonObjectSplitted[$j][$i].TenantGuid)'\"\n                    $sqlStatement += \", $($jsonObjectSplitted[$j][$i].FitScore), '$tagsString', '$($jsonObjectSplitted[$j][$i].DetailsURL)')\"\n                    if ($i -ne ($jsonObjectSplitted[$j].Count-1))\n                    {\n                        $sqlStatement += \",\"\n                    }\n                }\n\n                $Conn2 = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n                $Conn2.Open() \n        \n                $Cmd=new-object system.Data.SqlClient.SqlCommand\n                $Cmd.Connection = $Conn2\n                $Cmd.CommandText = $sqlStatement\n                $Cmd.CommandTimeout=120 \n                try\n                {\n                    $Cmd.ExecuteReader()\n                }\n                catch\n                {\n                    Write-Output \"Failed statement: $sqlStatement\"\n                    throw\n                }\n        \n                $Conn2.Close()                \n            \n                $linesProcessed += $currentObjectLines\n                Write-Output \"Processed $linesProcessed lines...\"\n                if ($j -eq ($jsonObjectSplitted.Count - 1)) {\n                    $lastProcessedLine = -1    \n                }\n                else {\n                    $lastProcessedLine = $linesProcessed - 1   \n                }\n            \n                $updatedLastProcessedLine = $lastProcessedLine\n                $updatedLastProcessedDateTime = $lastProcessedDateTime\n                if ($j -eq ($jsonObjectSplitted.Count - 1)) {\n                    $updatedLastProcessedDateTime = $newProcessedTime\n                }\n                $lastProcessedDateTime = $updatedLastProcessedDateTime\n                Write-Output \"Updating last processed time / line to $($updatedLastProcessedDateTime) / $updatedLastProcessedLine\"\n                $sqlStatement = \"UPDATE [$SqlServerIngestControlTable] SET LastProcessedLine = $updatedLastProcessedLine, LastProcessedDateTime = '$updatedLastProcessedDateTime' WHERE StorageContainerName = '$storageAccountSinkContainer'\"\n                $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n                $Conn.Open() \n                $Cmd=new-object system.Data.SqlClient.SqlCommand\n                $Cmd.Connection = $Conn\n                $Cmd.CommandText = $sqlStatement\n                $Cmd.CommandTimeout=$SqlTimeout \n                $Cmd.ExecuteReader()\n                $Conn.Close()\n            }\n            else\n            {\n                $linesProcessed += $currentObjectLines  \n            }        \n        }\n    }\n\n    Remove-Item -Path $blob.Name -Force\n}\n\nWrite-Output \"DONE\""
  },
  {
    "path": "runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$sharedKey = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceKey\"\n$LogAnalyticsChunkSize = [int] (Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsChunkSize\" -ErrorAction SilentlyContinue)\nif (-not($LogAnalyticsChunkSize -gt 0))\n{\n    $LogAnalyticsChunkSize = 6000\n}\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$SqlTimeout = 300\n$FiltersTable = \"Filters\"\n\n#region Functions\n\n# Function to create the authorization signature\nFunction Build-OMSSignature ($workspaceId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) {\n    $xHeaders = \"x-ms-date:\" + $date\n    $stringToHash = $method + \"`n\" + $contentLength + \"`n\" + $contentType + \"`n\" + $xHeaders + \"`n\" + $resource\n    $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)\n    $keyBytes = [Convert]::FromBase64String($sharedKey)\n    $sha256 = New-Object System.Security.Cryptography.HMACSHA256\n    $sha256.Key = $keyBytes\n    $calculatedHash = $sha256.ComputeHash($bytesToHash)\n    $encodedHash = [Convert]::ToBase64String($calculatedHash)\n    $authorization = 'SharedKey {0}:{1}' -f $workspaceId, $encodedHash\n    return $authorization\n}\n\n# Function to create and post the request\nFunction Post-OMSData($workspaceId, $sharedKey, $body, $logType, $TimeStampField, $AzureEnvironment) {\n    $method = \"POST\"\n    $contentType = \"application/json\"\n    $resource = \"/api/logs\"\n    $rfc1123date = [DateTime]::UtcNow.ToString(\"r\")\n    $contentLength = $body.Length\n    $signature = Build-OMSSignature `\n        -workspaceId $workspaceId `\n        -sharedKey $sharedKey `\n        -date $rfc1123date `\n        -contentLength $contentLength `\n        -method $method `\n        -contentType $contentType `\n        -resource $resource\n    \n    $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.com\" + $resource + \"?api-version=2016-04-01\"\n    if ($AzureEnvironment -eq \"AzureChinaCloud\")\n    {\n        $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.cn\" + $resource + \"?api-version=2016-04-01\"\n    }\n    if ($AzureEnvironment -eq \"AzureUSGovernment\")\n    {\n        $uri = \"https://\" + $workspaceId + \".ods.opinsights.azure.us\" + $resource + \"?api-version=2016-04-01\"\n    }\n    if ($AzureEnvironment -eq \"AzureGermanCloud\")\n    {\n        throw \"Azure Germany isn't suported for the Log Analytics Data Collector API\"\n    }\n\n    $OMSheaders = @{\n        \"Authorization\"        = $signature;\n        \"Log-Type\"             = $logType;\n        \"x-ms-date\"            = $rfc1123date;\n        \"time-generated-field\" = $TimeStampField;\n    }\n\n    Try {\n\n        $response = Invoke-WebRequest -Uri $uri -Method POST  -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000\n    }\n    catch {\n        if ($_.Exception.Response.StatusCode.Value__ -eq 401) {            \n            \"REAUTHENTICATING\"\n\n            $response = Invoke-WebRequest -Uri $uri -Method POST  -ContentType $contentType -Headers $OMSheaders -Body $body -UseBasicParsing -TimeoutSec 1000\n        }\n        else\n        {\n            return $_.Exception.Response.StatusCode.Value__\n        }\n    }\n\n    return $response.StatusCode    \n}\n#endregion Functions\n\nWrite-Output \"Getting excluded recommendation sub-type IDs...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$FiltersTable] WHERE IsEnabled = 1 AND (FilterEndDate IS NULL OR FilterEndDate > GETDATE())\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $filters = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($filters) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$filterObjects = @()\n\n$filterObject = New-Object PSObject -Property @{\n    Timestamp = $timestamp\n    FilterId = (New-Guid).Guid\n    RecommendationSubTypeId = [System.Guid]::empty.Guid\n    FilterType = \"Dummy\"\n    InstanceId = [System.Guid]::empty.Guid\n    InstanceName = \"Dummy\"\n    FilterStartDate = \"2019-01-01T00:00:00.000Z\"\n    FilterEndDate = \"2199-12-31T23:59:59.000Z\"\n    Author = \"AOE\"\n    Notes = \"This is a dummy suppression required to build the full suppressions schema in Log Analytics\"\n}\n$filterObjects += $filterObject\n\nforeach ($filter in $filters)\n{\n    $filterEndDate = $null\n    if (-not([string]::IsNullOrEmpty($filter.FilterEndDate)))\n    {\n        Write-Output $filter.FilterEndDate\n        $filterEndDate = $filter.FilterEndDate.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n    }\n    else\n    {\n        $filterEndDate = \"2199-12-31T23:59:59.000Z\"\n    }\n\n    $filterStartDate = $null\n    if (-not([string]::IsNullOrEmpty($filter.FilterStartDate)))\n    {\n        $filterStartDate = $filter.FilterStartDate.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n    }\n    else\n    {\n        $filterStartDate = \"2019-01-01T00:00:00.000Z\"\n    }\n\n    $instanceId = $null\n    $instanceName = $null\n    $ObjectGuid = [System.Guid]::empty       \n    if ([System.Guid]::TryParse($filter.InstanceId, [System.Management.Automation.PSReference]$ObjectGuid))\n    {\n        $instanceId = $filter.InstanceId\n    }\n    else\n    {\n        $instanceName = $filter.InstanceId\n    }\n\n    $filterObject = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        FilterId = $filter.FilterId\n        RecommendationSubTypeId = $filter.RecommendationSubTypeId\n        FilterType = $filter.FilterType\n        InstanceId = $instanceId\n        InstanceName = $instanceName\n        FilterStartDate = $filterStartDate\n        FilterEndDate = $filterEndDate\n        Author = $filter.Author\n        Notes = $filter.Notes\n    }\n    $filterObjects += $filterObject\n}\n\n$filtersJson = $filterObjects | ConvertTo-Json\n\n$LogAnalyticsSuffix = \"SuppressionsV1\"\n$logname = $lognamePrefix + $LogAnalyticsSuffix\n\n$res = Post-OMSData -workspaceId $workspaceId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($filtersJson)) -logType $logname -TimeStampField \"Timestamp\" -AzureEnvironment $cloudEnvironment\nIf ($res -ge 200 -and $res -lt 300) {\n    Write-Output \"Succesfully uploaded $($filterObjects.Count) $LogAnalyticsSuffix rows to Log Analytics\"    \n}\nElse {\n    Write-Warning \"Failed to upload $($filterObjects.Count) $LogAnalyticsSuffix rows. Error code: $res\"\n    throw\n}\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$expiringCredsDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationAADMinCredValidityDays\")\n$notExpiringCredsDays = ([int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationAADMaxCredValidityYears\")) * 365\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('AADObjects')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$aadObjectsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AADObjects' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $aadObjectsTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 1\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\n# Execute the expiring creds recommendation query against Log Analytics\n\n$baseQuery = @\" \n    let expiryInterval = $($expiringCredsDays)d;\n    let AppsAndKeys = materialize ($aadObjectsTableName\n    | where TimeGenerated > ago(1d)\n    | where ObjectType_s in ('Application','ServicePrincipal')\n    | where ObjectSubType_s != 'ManagedIdentity'\n    | where Keys_s startswith '['\n    | extend Keys = parse_json(Keys_s)\n    | project-away Keys_s\n    | mv-expand Keys\n    | evaluate bag_unpack(Keys)\n    | union ( \n        $aadObjectsTableName\n        | where TimeGenerated > ago(1d)\n        | where ObjectType_s in ('Application','ServicePrincipal')\n        | where ObjectSubType_s != 'ManagedIdentity'\n        | where isnotempty(Keys_s) and Keys_s !startswith '['\n        | extend Keys = parse_json(Keys_s)\n        | project-away Keys_s\n        | evaluate bag_unpack(Keys)\n    )\n    );\n    let ExpirationInRisk = AppsAndKeys\n    | where EndDate < now()+expiryInterval\n    | project ApplicationId_g, KeyId, RiskDate = EndDate;\n    let NotInRisk = AppsAndKeys\n    | where EndDate > now()+expiryInterval\n    | project ApplicationId_g, KeyId, ComfortDate = EndDate;\n    let ApplicationsInRisk = ExpirationInRisk\n    | join kind=leftouter ( NotInRisk ) on ApplicationId_g\n    | where isempty(ComfortDate)\n    | summarize ExpiresOn = max(RiskDate) by ApplicationId_g;\n    AppsAndKeys\n    | join kind=inner (ApplicationsInRisk) on ApplicationId_g\n    | summarize ExpiresOn = max(EndDate) by ApplicationId_g, ObjectType_s, DisplayName_s, Cloud_s, KeyType, TenantGuid_g\n    | order by ExpiresOn desc\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.ApplicationId_g\n    $detailsURL = \"https://portal.azure.$azureTld/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/$queryInstanceId\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"ObjectType\"] = $result.ObjectType_s\n    $additionalInfoDictionary[\"KeyType\"] = $result.KeyType\n    $additionalInfoDictionary[\"ExpiresOn\"] = $result.ExpiresOn\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.AzureActiveDirectory/objects\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"AADExpiringCredentials\"\n        RecommendationSubTypeId = \"3292c489-2782-498b-aad0-a4cef50f6ca2\"\n        RecommendationDescription = \"Microsoft Entra application with credentials expired or about to expire\"\n        RecommendationAction = \"Update the Microsoft Entra application credential before the expiration date\"\n        InstanceId = $result.ApplicationId_g\n        InstanceName = $result.DisplayName_s\n        AdditionalInfo = $additionalInfoDictionary\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"aadexpiringcerts-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\n# Execute the not expiring in less than X years creds recommendation query against Log Analytics\n\n$baseQuery = @\" \n    let expiryInterval = $($notExpiringCredsDays)d;\n    let AppsAndKeys = materialize ($aadObjectsTableName\n    | where TimeGenerated > ago(1d)\n    | where ObjectSubType_s != 'ManagedIdentity'\n    | where Keys_s startswith '['\n    | extend Keys = parse_json(Keys_s)\n    | project-away Keys_s\n    | mv-expand Keys\n    | evaluate bag_unpack(Keys)\n    | union ( \n        $aadObjectsTableName\n        | where TimeGenerated > ago(1d)\n        | where ObjectSubType_s != 'ManagedIdentity'\n        | where isnotempty(Keys_s) and Keys_s !startswith '['\n        | extend Keys = parse_json(Keys_s)\n        | project-away Keys_s\n        | evaluate bag_unpack(Keys)\n    )\n    );\n    AppsAndKeys\n    | where EndDate > now()+expiryInterval\n    | project ApplicationId_g, ObjectType_s, DisplayName_s, Cloud_s, KeyType, TenantGuid_g, EndDate\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.ApplicationId_g\n    $detailsURL = \"https://portal.azure.$azureTld/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Credentials/appId/$queryInstanceId\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"ObjectType\"] = $result.ObjectType_s\n    $additionalInfoDictionary[\"KeyType\"] = $result.KeyType\n    $additionalInfoDictionary[\"ExpiresOn\"] = $result.EndDate\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Security\"\n        ImpactedArea = \"Microsoft.AzureActiveDirectory/objects\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"AADNotExpiringCredentials\"\n        RecommendationSubTypeId = \"ecd969c8-3f16-481a-9577-5ed32e5e1a1d\"\n        RecommendationDescription = \"Microsoft Entra application with credentials expiration not set or too far in time\"\n        RecommendationAction = \"Update the Microsoft Entra application credential with a shorter expiration date\"\n        InstanceId = $result.ApplicationId_g\n        InstanceName = $result.DisplayName_s\n        AdditionalInfo = $additionalInfoDictionary\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"aadnotexpiringcerts-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$assignmentsPercentageThresholdVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($assignmentsPercentageThresholdVar) -or $assignmentsPercentageThresholdVar -eq 0)\n{\n    $assignmentsPercentageThreshold = 80\n}\nelse\n{\n    $assignmentsPercentageThreshold = [int] $assignmentsPercentageThresholdVar\n}\n\n$assignmentsSubscriptionsLimitVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationRBACSubscriptionsAssignmentsLimit\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($assignmentsSubscriptionsLimitVar) -or $assignmentsSubscriptionsLimitVar -eq 0)\n{\n    $assignmentsSubscriptionsLimit = 4000\n}\nelse\n{\n    $assignmentsSubscriptionsLimit = [int] $assignmentsSubscriptionsLimitVar\n}\n\n$assignmentsMgmtGroupsLimitVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationRBACMgmtGroupsAssignmentsLimit\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($assignmentsMgmtGroupsLimitVar) -or $assignmentsMgmtGroupsLimitVar -eq 0)\n{\n    $assignmentsMgmtGroupsLimit = 500\n}\nelse\n{\n    $assignmentsMgmtGroupsLimit = [int] $assignmentsMgmtGroupsLimitVar\n}\n\n$rgPercentageThresholdVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($rgPercentageThresholdVar) -or $rgPercentageThresholdVar -eq 0)\n{\n    $rgPercentageThreshold = 80\n}\nelse\n{\n    $rgPercentageThreshold = [int] $rgPercentageThresholdVar\n}\n\n$rgLimitVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationResourceGroupsPerSubLimit\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($rgLimitVar) -or $rgLimitVar -eq 0)\n{\n    $rgLimit = 980\n}\nelse\n{\n    $rgLimit = [int] $rgLimitVar\n}\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('RBACAssignments','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$rbacTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'RBACAssignments' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $rbacTableName and $subscriptionsTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\n$assignmentsThreshold = $assignmentsSubscriptionsLimit * ($assignmentsPercentageThreshold / 100)\n\nWrite-Output \"Looking for subscriptions with more than $assignmentsPercentageThreshold% of the $assignmentsSubscriptionsLimit RBAC assignments limit...\"\n\n$baseQuery = @\"\n    $rbacTableName\n    | where TimeGenerated > ago(1d) and Model_s == 'AzureRM' and Scope_s startswith '/subscriptions/'\n    | extend SubscriptionGuid_g = tostring(split(Scope_s, '/')[2])\n    | summarize AssignmentsCount=count() by SubscriptionGuid_g, TenantGuid_g, Cloud_s\n    | join kind=leftouter ( \n       $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s, Tags_s, InstanceId_s \n    ) on SubscriptionGuid_g\n    | where AssignmentsCount >= $assignmentsThreshold    \n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/users\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"assignmentsCount\"] = $result.AssignmentsCount\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Resources/subscriptions\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"HighRBACAssignmentsSubscriptions\"\n        RecommendationSubTypeId = \"c6a88d8c-3242-44b0-9793-c91897ef68bc\"\n        RecommendationDescription = \"Subscriptions close to the maximum limit of RBAC assignments\"\n        RecommendationAction = \"Remove unneeded RBAC assignments or use group-based (or nested group-based) assignments\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.SubscriptionName\n        AdditionalInfo = $additionalInfoDictionary\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"subscriptionsrbaclimits-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\n$assignmentsThreshold = $assignmentsMgmtGroupsLimit * ($assignmentsPercentageThreshold / 100)\n\nWrite-Output \"Looking for management groups with more than $assignmentsPercentageThreshold% of the $assignmentsMgmtGroupsLimit RBAC assignments limit...\"\n\n$baseQuery = @\"\n    $rbacTableName\n    | where TimeGenerated > ago(1d) and Model_s == 'AzureRM' and Scope_s has 'managementGroups'\n    | extend ManagementGroupId = tostring(split(Scope_s, '/')[4])\n    | summarize AssignmentsCount=count() by ManagementGroupId, TenantGuid_g, Scope_s, Cloud_s\n    | where AssignmentsCount >= $assignmentsThreshold        \n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/blade/Microsoft_Azure_ManagementGroups/ManagementGroupBrowseBlade/MGBrowse_overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"assignmentsCount\"] = $result.AssignmentsCount\n\n    $fitScore = 5\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Management/managementGroups\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"HighRBACAssignmentsManagementGroups\"\n        RecommendationSubTypeId = \"b36dea3e-ef21-45a9-a704-6f629fab236d\"\n        RecommendationDescription = \"Management Groups close to the maximum limit of RBAC assignments\"\n        RecommendationAction = \"Remove unneeded RBAC assignments or use group-based (or nested group-based) assignments\"\n        InstanceId = $result.Scope_s\n        InstanceName = $result.ManagementGroupId\n        AdditionalInfo = $additionalInfoDictionary\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"mgmtgroupsrbaclimits-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\n$rgThreshold = $rgLimit * ($rgPercentageThreshold / 100)\n\nWrite-Output \"Looking for subscriptions with more than $rgPercentageThreshold% of the $rgLimit Resource Groups limit...\"\n\n$baseQuery = @\"\n    $subscriptionsTableName\n    | where TimeGenerated > ago(1d)\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions/resourceGroups' \n    | summarize RGCount=count() by SubscriptionGuid_g, TenantGuid_g, Cloud_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s, Tags_s, InstanceId_s \n    ) on SubscriptionGuid_g\n    | where RGCount >= $rgThreshold    \n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/resourceGroups\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"resourceGroupsCount\"] = $result.RGCount\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Resources/subscriptions\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"HighResourceGroupCountSubscriptions\"\n        RecommendationSubTypeId = \"4468da8d-1e72-4998-b6d2-3bc38ddd9330\"\n        RecommendationDescription = \"Subscriptions close to the maximum limit of resource groups\"\n        RecommendationAction = \"Remove unneeded resource groups or split your resource groups across multiple subscriptions\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.SubscriptionName\n        AdditionalInfo = $additionalInfoDictionary\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"subscriptionsrglimits-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n# must be less than or equal to the advisor exports frequency\n$daysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendAdvisorPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($daysBackwards -gt 0)) {\n    $daysBackwards = 7\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$CategoryFilter = Get-AutomationVariable -Name  \"AzureOptimization_AdvisorFilter\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($CategoryFilter))\n{\n    $CategoryFilter = \"HighAvailability,Security,Performance,OperationalExcellence\" # comma-separated list of categories\n}\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n$FiltersTable = \"Filters\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVirtualMachine','AzureAdvisor','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$advisorTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureAdvisor' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $subscriptionsTableName and $advisorTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\nWrite-Output \"Getting excluded recommendation sub-type IDs...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$FiltersTable] WHERE FilterType = 'Exclude' AND IsEnabled = 1 AND (FilterEndDate IS NULL OR FilterEndDate > GETDATE())\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $filters = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($filters) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n# Execute the recommendation query against Log Analytics\n\n$FinalCategoryFilter = \"\"\n\nif (-not([string]::IsNullOrEmpty($CategoryFilter)))\n{\n    $categories = $CategoryFilter.Split(',')\n    for ($i = 0; $i -lt $categories.Count; $i++)\n    {\n        $categories[$i] = \"'\" + $categories[$i] + \"'\"\n    }    \n    $FinalCategoryFilter = \" and Category in (\" + ($categories -join \",\") + \")\"\n}\n\n$baseQuery = @\"\nlet advisorInterval = $($daysBackwards)d;\n$advisorTableName \n| where todatetime(TimeGenerated) > ago(advisorInterval)$FinalCategoryFilter\n| extend AdvisorRecIdIndex = indexof(InstanceId_s, '/providers/microsoft.advisor/recommendations')\n| extend InstanceName_s = iif(isnotempty(InstanceName_s),InstanceName_s,iif(AdvisorRecIdIndex > 0, split(substring(InstanceId_s, 0, AdvisorRecIdIndex),'/')[-1], split(InstanceId_s,'/')[-1]))\n| summarize by InstanceId_s, InstanceName_s, Category, Description_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroup, Cloud_s, AdditionalInfo_s, RecommendationText_s, ImpactedArea_s, Impact_s, RecommendationTypeId_g, Tags_s\n| join kind=leftouter ( \n    $subscriptionsTableName\n    | where TimeGenerated > ago(1d) \n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n) on SubscriptionGuid_g\n\"@\n\nWrite-Output \"Getting $CategoryFilter recommendations for $($daysBackwards)d Advisor...\"\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $daysBackwards) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    throw \"Execution aborted\"\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nWrite-Output \"Generating fit score...\"\n\nforeach ($result in $results) {  \n\n    if ($filters | Where-Object { $_.RecommendationSubTypeId -eq $result.RecommendationTypeId_g})\n    {\n        continue\n    }\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $additionalInfoDictionary = @{}\n    if (-not([string]::IsNullOrEmpty($result.AdditionalInfo_s)))\n    {\n        ($result.AdditionalInfo_s | ConvertFrom-Json).PsObject.Properties | ForEach-Object { $additionalInfoDictionary[$_.Name] = $_.Value }\n    }\n    \n    $fitScore = 5\n\n    $queryInstanceId = $result.InstanceId_s\n\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $recommendationSubType = \"Advisor\" + $result.Category\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp                   = $timestamp\n        Cloud                       = $result.Cloud_s\n        Category                    = $result.Category\n        ImpactedArea                = $result.ImpactedArea_s\n        Impact                      = $result.Impact_s\n        RecommendationType          = \"BestPractices\"\n        RecommendationSubType       = $recommendationSubType\n        RecommendationSubTypeId     = $result.RecommendationTypeId_g\n        RecommendationDescription   = $result.Description_s.Replace(\"'\",\"\")\n        RecommendationAction        = $result.RecommendationText_s.Replace(\"'\",\"\")\n        InstanceId                  = $result.InstanceId_s\n        InstanceName                = $result.InstanceName_s\n        AdditionalInfo              = $additionalInfoDictionary\n        ResourceGroup               = $result.ResourceGroup\n        SubscriptionGuid            = $result.SubscriptionGuid_g\n        SubscriptionName            = $result.SubscriptionName\n        TenantGuid                  = $result.TenantGuid_g\n        FitScore                    = $fitScore\n        Tags                        = $tags\n        DetailsURL                  = $detailsURL\n    }\n\n    $recommendations += $recommendation    \n}\n\n# Export the recommendations as JSON to blob storage\n\nWrite-Output \"Exporting final $($recommendations.Count) results as a JSON file...\"\n\n$fileDate = $datetime.ToString(\"yyyyMMdd\")\n$jsonExportPath = \"advisor-asis-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\nWrite-Output \"Uploading $jsonExportPath to blob storage...\"\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\" };\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-SkuHourlyPrice {\n    param (\n        [object[]] $SKUPriceSheet,\n        [string] $SKUName\n    )\n\n    $skuPriceObject = $null\n\n    if ($SKUPriceSheet)\n    {\n        $skuNameParts = $SKUName.Split('_')\n\n        if ($skuNameParts.Count -eq 3) # e.g., Standard_D1_v2\n        {\n            $skuNameFilter = \"*\" + $skuNameParts[1] + \" *\"\n            $skuVersionFilter = \"*\" + $skuNameParts[2]\n            $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter `\n             -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' `\n             -and $_.MeterName_s -like $skuVersionFilter -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 }\n            \n            if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n            {\n                $skuPriceObject = $skuPrices[0]\n            }\n            if ($skuPrices.Count -gt 2) # D1-like scenarios\n            {\n                $skuFilter = \"*\" + $skuNameParts[1] + \" \" + $skuNameParts[2] + \"*\"\n                $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilter }\n    \n                if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n                {\n                    $skuPriceObject = $skuPrices[0]\n                }\n            }\n        }\n    \n        if ($skuNameParts.Count -eq 2) # e.g., Standard_D1\n        {\n            $skuNameFilter = \"*\" + $skuNameParts[1] + \"*\"\n    \n            $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter `\n             -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' `\n             -and $_.MeterName_s -notlike '* v*' -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 }\n            \n            if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n            {\n                $skuPriceObject = $skuPrices[0]\n            }\n            if ($skuPrices.Count -gt 2) # D1-like scenarios\n            {\n                $skuFilterLeft = \"*\" + $skuNameParts[1] + \"/*\"\n                $skuFilterRight = \"*/\" + $skuNameParts[1] + \"*\"\n                $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilterLeft -or $_.MeterName_s -like $skuFilterRight }\n                \n                if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n                {\n                    $skuPriceObject = $skuPrices[0]\n                }\n            }\n        }    \n    }\n\n    $targetHourlyPrice = [double]::MaxValue\n    if ($null -ne $skuPriceObject)\n    {\n        $targetUnitHours = [int] (Select-String -InputObject $skuPriceObject.UnitOfMeasure_s -Pattern \"^\\d+\").Matches[0].Value\n        if ($targetUnitHours -gt 0)\n        {\n            $targetHourlyPrice = [double] ($skuPriceObject.UnitPrice_s / $targetUnitHours)\n        }\n    }\n\n    return $targetHourlyPrice\n}\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\"\n\n# must be less than or equal to the advisor exports frequency\n$daysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendAdvisorPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($daysBackwards -gt 0)) {\n    $daysBackwards = 7\n}\n\n$perfDaysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($perfDaysBackwards -gt 0)) {\n    $perfDaysBackwards = 7\n}\n\n$perfTimeGrain = Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfTimeGrain\" -ErrorAction SilentlyContinue\nif (-not($perfTimeGrain)) {\n    $perfTimeGrain = \"1h\"\n}\n\n# percentiles variables\n$cpuPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileCpu\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentile -gt 0)) {\n    $cpuPercentile = 99\n}\n$memoryPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileMemory\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentile -gt 0)) {\n    $memoryPercentile = 99\n}\n$networkPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileNetwork\" -ErrorAction SilentlyContinue)\nif (-not($networkPercentile -gt 0)) {\n    $networkPercentile = 99\n}\n$diskPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileDisk\" -ErrorAction SilentlyContinue)\nif (-not($diskPercentile -gt 0)) {\n    $diskPercentile = 99\n}\n\n# perf thresholds variables\n$cpuPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentageThreshold -gt 0)) {\n    $cpuPercentageThreshold = 30\n}\n$memoryPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdMemoryPercentage\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentageThreshold -gt 0)) {\n    $memoryPercentageThreshold = 50\n}\n$networkMpbsThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdNetworkMbps\" -ErrorAction SilentlyContinue)\nif (-not($networkMpbsThreshold -gt 0)) {\n    $networkMpbsThreshold = 750\n}\n\n# perf thresholds variables (shutdown)\n$cpuPercentageShutdownThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuShutdownPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentageShutdownThreshold -gt 0)) {\n    $cpuPercentageShutdownThreshold = 5\n}\n$memoryPercentageShutdownThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdMemoryShutdownPercentage\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentageShutdownThreshold -gt 0)) {\n    $memoryPercentageShutdownThreshold = 100\n}\n$networkMpbsShutdownThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdNetworkShutdownMbps\" -ErrorAction SilentlyContinue )\nif (-not($networkMpbsShutdownThreshold -gt 0)) {\n    $networkMpbsShutdownThreshold = 10\n}\n\n$rightSizeRecommendationId = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationAdvisorCostRightSizeId\" -ErrorAction SilentlyContinue\nif (-not($rightSizeRecommendationId)) {\n    $rightSizeRecommendationId = 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974'\n}\n\n$additionalPerfWorkspaces = Get-AutomationVariable -Name  \"AzureOptimization_RightSizeAdditionalPerfWorkspaces\" -ErrorAction SilentlyContinue\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n$FiltersTable = \"Filters\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVirtualMachine','AzureAdvisor','AzureConsumption','ARGResourceContainers','Pricesheet')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null           \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$vmsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualMachine' }).LogAnalyticsSuffix + \"_CL\"\n$advisorTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureAdvisor' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n$pricesheetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'Pricesheet' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $vmsTableName, $subscriptionsTableName, $advisorTableName, $pricesheetTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\nWrite-Output \"Getting excluded recommendation sub-type IDs...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$FiltersTable] WHERE FilterType = 'Exclude' AND IsEnabled = 1 AND (FilterEndDate IS NULL OR FilterEndDate > GETDATE())\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $filters = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($filters) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n\nWrite-Output \"Getting Virtual Machine SKUs for the $referenceRegion region...\"\n# Get all the VM SKUs information for the reference Azure region\n$skus = Get-AzComputeResourceSku -Location $referenceRegion | Where-Object { $_.ResourceType -eq \"virtualMachines\" }\n\nWrite-Output \"Getting the current Pricesheet...\"\n\nif ($cloudEnvironment -eq \"AzureCloud\")\n{\n    $pricesheetRegion = \"EU West\"\n}\n\ntry \n{\n    $pricesheetEntries = @()\n\n    $baseQuery = @\"\n    $pricesheetTableName\n    | where TimeGenerated > ago(14d)\n    | where MeterCategory_s == 'Virtual Machines' and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption'\n    | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s\n\"@    \n\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 14) -Wait 600 -IncludeStatistics\n    $pricesheetEntries = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    \n    Write-Output \"Query finished with $($pricesheetEntries.Count) results.\"   \n    Write-Output \"Query statistics: $($queryResults.Statistics.query)\"    \n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    Write-Output \"Consumption pricesheet not available, will estimate savings based in cores count...\"\n}\n\n$linuxMemoryPerfAdditionalWorkspaces = \"\"\n$windowsMemoryPerfAdditionalWorkspaces = \"\"\n$processorPerfAdditionalWorkspaces = \"\"\n$windowsNetworkPerfAdditionalWorkspaces = \"\"\n$diskPerfAdditionalWorkspaces = \"\"\nif ($additionalPerfWorkspaces)\n{\n    $additionalWorkspaces = $additionalPerfWorkspaces.Split(\",\")\n    foreach ($additionalWorkspace in $additionalWorkspaces) {\n        $additionalWorkspace = $additionalWorkspace.Trim()\n        $linuxMemoryPerfAdditionalWorkspaces += @\"\n        | union ( workspace('$additionalWorkspace').Perf \n        | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n        | where CounterName == '% Used Memory'\n        | extend WorkspaceId = TenantId \n        | summarize hint.strategy=shuffle PMemoryPercentage = percentile(CounterValue, memoryPercentileValue) by _ResourceId, WorkspaceId)\n\"@\n        $windowsMemoryPerfAdditionalWorkspaces += @\"\n        | union ( workspace('$additionalWorkspace').Perf \n        | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n        | where CounterName == 'Available MBytes' \n        | extend WorkspaceId = TenantId \n        | project TimeGenerated, MemoryAvailableMBs = CounterValue, _ResourceId, WorkspaceId)\n\"@\n        $processorPerfAdditionalWorkspaces += @\"\n        | union ( workspace('$additionalWorkspace').Perf \n        | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n        | where ObjectName == 'Processor' and CounterName == '% Processor Time' and InstanceName == '_Total' \n        | extend WorkspaceId = TenantId \n        | summarize hint.strategy=shuffle PCPUPercentage = percentile(CounterValue, cpuPercentileValue) by _ResourceId, WorkspaceId)\n\"@\n        $windowsNetworkPerfAdditionalWorkspaces += @\"\n        | union ( workspace('$additionalWorkspace').Perf \n        | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n        | where CounterName == 'Bytes Total/sec' \n        | extend WorkspaceId = TenantId \n        | summarize hint.strategy=shuffle PCounter = percentile(CounterValue, networkPercentileValue) by InstanceName, _ResourceId, WorkspaceId\n        | summarize PNetwork = sum(PCounter) by _ResourceId, WorkspaceId)\n\"@\n        $diskPerfAdditionalWorkspaces += @\"\n        | union ( workspace('$additionalWorkspace').Perf\n        | where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n        | where CounterName in ('Disk Reads/sec', 'Disk Writes/sec', 'Disk Read Bytes/sec', 'Disk Write Bytes/sec') and InstanceName !in ('_Total', 'D:', '/mnt/resource', '/mnt')\n        | extend WorkspaceId = TenantId \n        | summarize hint.strategy=shuffle PCounter = percentile(CounterValue, diskPercentileValue) by bin(TimeGenerated, perfTimeGrain), CounterName, InstanceName, _ResourceId, WorkspaceId\n        | summarize SumPCounter = sum(PCounter) by CounterName, TimeGenerated, _ResourceId, WorkspaceId\n        | summarize MaxPReadIOPS = maxif(SumPCounter, CounterName == 'Disk Reads/sec'), \n                    MaxPWriteIOPS = maxif(SumPCounter, CounterName == 'Disk Writes/sec'), \n                    MaxPReadMiBps = (maxif(SumPCounter, CounterName == 'Disk Read Bytes/sec') / 1024 / 1024), \n                    MaxPWriteMiBps = (maxif(SumPCounter, CounterName == 'Disk Write Bytes/sec') / 1024 / 1024) by _ResourceId, WorkspaceId)\n\"@\n    }\n}\n\n# Execute the recommendation query against Log Analytics\n\n$baseQuery = @\"\nlet advisorInterval = $($daysBackwards)d;\nlet perfInterval = $($perfDaysBackwards)d;\nlet perfTimeGrain = $perfTimeGrain;\nlet cpuPercentileValue = $cpuPercentile;\nlet memoryPercentileValue = $memoryPercentile;\nlet networkPercentileValue = $networkPercentile;\nlet diskPercentileValue = $diskPercentile;\nlet rightSizeRecommendationId = '$rightSizeRecommendationId';\nlet billingInterval = 30d;\nlet etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); \nlet stime = etime-billingInterval; \nlet RightSizeInstanceIds = materialize($advisorTableName \n| where todatetime(TimeGenerated) > ago(advisorInterval) and Category == 'Cost' and RecommendationTypeId_g == rightSizeRecommendationId\n| distinct InstanceId_s);\nlet LinuxMemoryPerf = Perf \n| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n| where CounterName == '% Used Memory' \n| extend WorkspaceId = TenantId \n| summarize hint.strategy=shuffle PMemoryPercentage = percentile(CounterValue, memoryPercentileValue) by _ResourceId, WorkspaceId$linuxMemoryPerfAdditionalWorkspaces;\nlet WindowsMemoryPerf = Perf \n| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n| where CounterName == 'Available MBytes' \n| extend WorkspaceId = TenantId \n| project TimeGenerated, MemoryAvailableMBs = CounterValue, _ResourceId, WorkspaceId$windowsMemoryPerfAdditionalWorkspaces;\nlet MemoryPerf = $vmsTableName \n| where TimeGenerated > ago(1d)\n| distinct InstanceId_s, MemoryMB_s\n| join kind=inner hint.strategy=broadcast (\n\tWindowsMemoryPerf\n) on `$left.InstanceId_s == `$right._ResourceId\n| extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 \n| summarize hint.strategy=shuffle PMemoryPercentage = percentile(MemoryPercentage, memoryPercentileValue) by _ResourceId, WorkspaceId\n| union LinuxMemoryPerf;\nlet ProcessorPerf = Perf \n| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n| where ObjectName == 'Processor' and CounterName == '% Processor Time' and InstanceName == '_Total' \n| extend WorkspaceId = TenantId \n| summarize hint.strategy=shuffle PCPUPercentage = percentile(CounterValue, cpuPercentileValue) by _ResourceId, WorkspaceId$processorPerfAdditionalWorkspaces;\nlet WindowsNetworkPerf = Perf \n| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n| where CounterName == 'Bytes Total/sec' \n| extend WorkspaceId = TenantId \n| summarize hint.strategy=shuffle PCounter = percentile(CounterValue, networkPercentileValue) by InstanceName, _ResourceId, WorkspaceId\n| summarize PNetwork = sum(PCounter) by _ResourceId, WorkspaceId$windowsNetworkPerfAdditionalWorkspaces;\nlet DiskPerf = Perf\n| where TimeGenerated > ago(perfInterval) and _ResourceId in (RightSizeInstanceIds) \n| where CounterName in ('Disk Reads/sec', 'Disk Writes/sec', 'Disk Read Bytes/sec', 'Disk Write Bytes/sec') and InstanceName !in ('_Total', 'D:', '/mnt/resource', '/mnt')\n| extend WorkspaceId = TenantId \n| summarize hint.strategy=shuffle PCounter = percentile(CounterValue, diskPercentileValue) by bin(TimeGenerated, perfTimeGrain), CounterName, InstanceName, _ResourceId, WorkspaceId\n| summarize SumPCounter = sum(PCounter) by CounterName, TimeGenerated, _ResourceId, WorkspaceId\n| summarize MaxPReadIOPS = maxif(SumPCounter, CounterName == 'Disk Reads/sec'), \n            MaxPWriteIOPS = maxif(SumPCounter, CounterName == 'Disk Writes/sec'), \n            MaxPReadMiBps = (maxif(SumPCounter, CounterName == 'Disk Read Bytes/sec') / 1024 / 1024), \n            MaxPWriteMiBps = (maxif(SumPCounter, CounterName == 'Disk Write Bytes/sec') / 1024 / 1024) by _ResourceId, WorkspaceId$diskPerfAdditionalWorkspaces;\n$advisorTableName \n| where todatetime(TimeGenerated) > ago(advisorInterval) and Category == 'Cost'\n| extend AdvisorRecIdIndex = indexof(InstanceId_s, '/providers/microsoft.advisor/recommendations')\n| extend InstanceName_s = iif(isnotempty(InstanceName_s),InstanceName_s,iif(AdvisorRecIdIndex > 0, split(substring(InstanceId_s, 0, AdvisorRecIdIndex),'/')[-1], split(InstanceId_s,'/')[-1]))\n| distinct InstanceId_s, InstanceName_s, Description_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroup, Cloud_s, AdditionalInfo_s, RecommendationText_s, ImpactedArea_s, Impact_s, RecommendationTypeId_g, Tags_s\n| join kind=leftouter (\n    $consumptionTableName\n    | where todatetime(Date_s) between (stime..etime)\n    | extend VMConsumedQuantity = iif(ResourceId contains 'virtualmachines' and MeterCategory_s == 'Virtual Machines', todouble(Quantity_s), 0.0)\n    | extend VMPrice = iif(ResourceId contains 'virtualmachines' and MeterCategory_s == 'Virtual Machines', todouble(EffectivePrice_s), 0.0)\n    | extend FinalCost = iif(ResourceId contains 'virtualmachines', VMPrice * VMConsumedQuantity, todouble(CostInBillingCurrency_s))\n    | extend InstanceId_s = tolower(ResourceId)\n    | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(VMConsumedQuantity) by InstanceId_s\n) on InstanceId_s\n| join kind=leftouter (\n    $vmsTableName \n    | where TimeGenerated > ago(1d) \n    | distinct InstanceId_s, NicCount_s, DataDiskCount_s\n) on InstanceId_s \n| where RecommendationTypeId_g != rightSizeRecommendationId or (RecommendationTypeId_g == rightSizeRecommendationId and toint(NicCount_s) >= 0 and toint(DataDiskCount_s) >= 0)\n| join kind=leftouter hint.strategy=broadcast ( MemoryPerf ) on `$left.InstanceId_s == `$right._ResourceId\n| join kind=leftouter hint.strategy=broadcast ( ProcessorPerf ) on `$left.InstanceId_s == `$right._ResourceId\n| join kind=leftouter hint.strategy=broadcast ( WindowsNetworkPerf ) on `$left.InstanceId_s == `$right._ResourceId\n| join kind=leftouter hint.strategy=broadcast ( DiskPerf ) on `$left.InstanceId_s == `$right._ResourceId\n| extend MaxPIOPS = MaxPReadIOPS + MaxPWriteIOPS, MaxPMiBps = MaxPReadMiBps + MaxPWriteMiBps\n| extend PNetworkMbps = PNetwork * 8 / 1000 / 1000\n| distinct Last30DaysCost, Last30DaysQuantity, InstanceId_s, InstanceName_s, Description_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroup, Cloud_s, AdditionalInfo_s, RecommendationText_s, ImpactedArea_s, Impact_s, RecommendationTypeId_g, NicCount_s, DataDiskCount_s, PMemoryPercentage, PCPUPercentage, PNetworkMbps, MaxPIOPS, MaxPMiBps, Tags_s, WorkspaceId\n| join kind=leftouter ( \n    $subscriptionsTableName\n    | where TimeGenerated > ago(1d)\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n) on SubscriptionGuid_g\n\"@\n\nWrite-Output \"Will run the following query (use this query against the LA workspace for troubleshooting): $baseQuery\"\n\nWrite-Output \"Getting cost recommendations for $($daysBackwards)d Advisor and $($perfDaysBackwards)d Perf history and a $perfTimeGrain time grain...\"\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    throw \"Execution aborted\"\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\n$skuPricesFound = @{}\n\nWrite-Output \"Generating fit score...\"\n\nforeach ($result in $results) {  \n\n    if ($filters | Where-Object { $_.RecommendationSubTypeId -eq $result.RecommendationTypeId_g})\n    {\n        continue\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n    \n    $additionalInfoDictionary = @{}\n    if (-not([string]::IsNullOrEmpty($result.AdditionalInfo_s)))\n    {\n        ($result.AdditionalInfo_s | ConvertFrom-Json).PsObject.Properties | ForEach-Object { $additionalInfoDictionary[$_.Name] = $_.Value }\n    }\n    \n    # Fixing reservation model inconsistencies\n    if (-not([string]::IsNullOrEmpty($additionalInfoDictionary[\"location\"])))\n    {\n        $additionalInfoDictionary[\"region\"] = $additionalInfoDictionary[\"location\"]\n    }\n    if (-not([string]::IsNullOrEmpty($additionalInfoDictionary[\"targetResourceCount\"])))\n    {\n        $additionalInfoDictionary[\"qty\"] = $additionalInfoDictionary[\"targetResourceCount\"]\n    }\n    if (-not([string]::IsNullOrEmpty($additionalInfoDictionary[\"vmSize\"])))\n    {\n        $additionalInfoDictionary[\"displaySKU\"] = $additionalInfoDictionary[\"vmSize\"]\n    }\n\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n\n    $fitScore = 5\n    $hasCpuRamPerfMetrics = $false\n\n    if ($additionalInfoDictionary.targetSku -and $result.RecommendationTypeId_g -eq $rightSizeRecommendationId) {\n        $additionalInfoDictionary[\"SupportsDataDisksCount\"] = \"true\"\n        $additionalInfoDictionary[\"DataDiskCount\"] = \"$($result.DataDiskCount_s)\"\n        $additionalInfoDictionary[\"SupportsNICCount\"] = \"true\"\n        $additionalInfoDictionary[\"NicCount\"] = \"$($result.NicCount_s)\"\n        $additionalInfoDictionary[\"SupportsIOPS\"] = \"true\"\n        $additionalInfoDictionary[\"MetricIOPS\"] = \"$($result.MaxPIOPS)\"\n        $additionalInfoDictionary[\"SupportsMiBps\"] = \"true\"\n        $additionalInfoDictionary[\"MetricMiBps\"] = \"$($result.MaxPMiBps)\"\n        $additionalInfoDictionary[\"BelowCPUThreshold\"] = \"true\"\n        $additionalInfoDictionary[\"MetricCPUPercentage\"] = \"$($result.PCPUPercentage)\"\n        $additionalInfoDictionary[\"BelowMemoryThreshold\"] = \"true\"\n        $additionalInfoDictionary[\"MetricMemoryPercentage\"] = \"$($result.PMemoryPercentage)\"\n        $additionalInfoDictionary[\"BelowNetworkThreshold\"] = \"true\"\n        $additionalInfoDictionary[\"MetricNetworkMbps\"] = \"$($result.PNetworkMbps)\"\n\n        $targetSku = $null\n        if ($additionalInfoDictionary.targetSku -ne \"Shutdown\") {\n            $currentSku = $skus | Where-Object { $_.Name -eq $additionalInfoDictionary.currentSku }\n            $currentSkuvCPUs = [int]($currentSku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value\n            $targetSku = $skus | Where-Object { $_.Name -eq $additionalInfoDictionary.targetSku }\n            $targetSkuvCPUs = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value\n            $targetMaxDataDiskCount = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'MaxDataDiskCount' }).Value\n            if ($targetMaxDataDiskCount -gt 0) {\n                if (-not([string]::isNullOrEmpty($result.DataDiskCount_s))) {\n                    if ([int]$result.DataDiskCount_s -gt $targetMaxDataDiskCount) {\n                        $fitScore = 1\n                        $additionalInfoDictionary[\"SupportsDataDisksCount\"] = \"false:needs$($result.DataDiskCount_s)-max$targetMaxDataDiskCount\"\n                    }\n                }\n                else {\n                    $fitScore -= 1\n                    $additionalInfoDictionary[\"SupportsDataDisksCount\"] = \"unknown:max$targetMaxDataDiskCount\"\n                }\n            }\n            else {\n                $fitScore -= 1\n                $additionalInfoDictionary[\"SupportsDataDisksCount\"] = \"unknown:needs$($result.DataDiskCount_s)\"\n            }\n            $targetMaxNICCount = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'MaxNetworkInterfaces' }).Value\n            if ($targetMaxNICCount -gt 0) {\n                if (-not([string]::isNullOrEmpty($result.NicCount_s))) {\n                    if ([int]$result.NicCount_s -gt $targetMaxNICCount) {\n                        $fitScore = 1\n                        $additionalInfoDictionary[\"SupportsNICCount\"] = \"false:needs$($result.NicCount_s)-max$targetMaxNICCount\"\n                    }\n                }\n                else {\n                    $fitScore -= 1\n                    $additionalInfoDictionary[\"SupportsNICCount\"] = \"unknown:max$targetMaxNICCount\"\n                }\n            }\n            else {\n                $fitScore -= 1\n                $additionalInfoDictionary[\"SupportsNICCount\"] = \"unknown:needs$($result.NicCount_s)\"\n            }\n            $targetUncachedDiskIOPS = [int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'UncachedDiskIOPS' }).Value\n            if ($targetUncachedDiskIOPS -gt 0) {\n                if (-not([string]::isNullOrEmpty($result.MaxPIOPS))) {\n                    if ([double]$result.MaxPIOPS -ge [double]$targetUncachedDiskIOPS) {\n                        $fitScore -= 1\n                        $additionalInfoDictionary[\"SupportsIOPS\"] = \"false:needs$($result.MaxPIOPS)-max$targetUncachedDiskIOPS\"            \n                    }\n                }\n                else {\n                    $fitScore -= 0.5\n                    $additionalInfoDictionary[\"SupportsIOPS\"] = \"unknown:max$targetUncachedDiskIOPS\"\n                }\n            }\n            else {\n                $fitScore -= 1\n                $additionalInfoDictionary[\"SupportsIOPS\"] = \"unknown:needs$($result.MaxPIOPS)\" \n            }\n            $targetUncachedDiskMiBps = [double]([int]($targetSku.Capabilities | Where-Object { $_.Name -eq 'UncachedDiskBytesPerSecond' }).Value) / 1024 / 1024\n            if ($targetUncachedDiskMiBps -gt 0) { \n                if (-not([string]::isNullOrEmpty($result.MaxPMiBps))) {\n                    if ([double]$result.MaxPMiBps -ge $targetUncachedDiskMiBps) {\n                        $fitScore -= 1    \n                        $additionalInfoDictionary[\"SupportsMiBps\"] = \"false:needs$($result.MaxPMiBps)-max$targetUncachedDiskMiBps\"                    \n                    }\n                }\n                else {\n                    $fitScore -= 0.5\n                    $additionalInfoDictionary[\"SupportsMiBps\"] = \"unknown:max$targetUncachedDiskMiBps\"\n                }\n            }\n            else {\n                $additionalInfoDictionary[\"SupportsMiBps\"] = \"unknown:needs$($result.MaxPMiBps)\"\n            }\n\n            $savingCoefficient = [double] $currentSkuvCPUs / $targetSkuvCPUs\n\n            if ($savingCoefficient -gt 1)\n            {\n                $targetSkuSavingsMonthly = [double]$result.Last30DaysCost - ([double]$result.Last30DaysCost / $savingCoefficient)\n            }\n            else\n            {\n                $targetSkuSavingsMonthly = [double]$result.Last30DaysCost / 2\n            }    \n\n            if ($targetSku -and $null -eq $skuPricesFound[$targetSku.Name])\n            {\n                $skuPricesFound[$targetSku.Name] = Find-SkuHourlyPrice -SKUName $targetSku.Name -SKUPriceSheet $pricesheetEntries\n            }\n\n            $tentativeTargetSkuSavingsMonthly = -1\n\n            if ($targetSku -and $skuPricesFound[$targetSku.Name] -gt 0 -and $skuPricesFound[$targetSku.Name] -lt [double]::MaxValue)\n            {\n                $targetSkuPrice = $skuPricesFound[$targetSku.Name]    \n\n                if ($null -eq $skuPricesFound[$currentSku.Name])\n                {\n                    $skuPricesFound[$currentSku.Name] = Find-SkuHourlyPrice -SKUName $currentSku.Name -SKUPriceSheet $pricesheetEntries\n                }\n\n                if ($skuPricesFound[$currentSku.Name] -gt 0)\n                {\n                    $currentSkuPrice = $skuPricesFound[$currentSku.Name]    \n                    $tentativeTargetSkuSavingsMonthly = ($currentSkuPrice * [double] $result.Last30DaysQuantity) - ($targetSkuPrice * [double] $result.Last30DaysQuantity)    \n                }\n                else\n                {\n                    $tentativeTargetSkuSavingsMonthly = [double]$result.Last30DaysCost - ($targetSkuPrice * [double] $result.Last30DaysQuantity)    \n                }\n            }\n\n            if ($tentativeTargetSkuSavingsMonthly -ge 0)\n            {\n                $targetSkuSavingsMonthly = $tentativeTargetSkuSavingsMonthly\n            }\n    \n            if ($targetSkuSavingsMonthly -eq [double]::PositiveInfinity)\n            {\n                $targetSkuSavingsMonthly = [double] $result.Last30DaysCost / 2\n            }\n    \n            $savingsMonthly = $targetSkuSavingsMonthly\n\n        }\n        else\n        {\n            $savingsMonthly = [double]$result.Last30DaysCost\n        }\n\n        $cpuThreshold = $cpuPercentageThreshold\n        $memoryThreshold = $memoryPercentageThreshold\n        $networkThreshold = $networkMpbsThreshold\n        if ($additionalInfoDictionary.targetSku -eq \"Shutdown\") {\n            $cpuThreshold = $cpuPercentageShutdownThreshold\n            $memoryThreshold = $memoryPercentageShutdownThreshold\n            $networkThreshold = $networkMpbsShutdownThreshold\n        }\n\n        if (-not([string]::isNullOrEmpty($result.PCPUPercentage))) {\n            if ([double]$result.PCPUPercentage -ge [double]$cpuThreshold) {\n                $fitScore -= 0.5    \n                $additionalInfoDictionary[\"BelowCPUThreshold\"] = \"false:needs$($result.PCPUPercentage)-max$cpuThreshold\"                    \n            }\n            $hasCpuRamPerfMetrics = $true\n        }\n        else {\n            $fitScore -= 0.5\n            $additionalInfoDictionary[\"BelowCPUThreshold\"] = \"unknown:max$cpuThreshold\"\n        }\n        if (-not([string]::isNullOrEmpty($result.PMemoryPercentage))) {\n            if ([double]$result.PMemoryPercentage -ge [double]$memoryThreshold) {\n                $fitScore -= 0.5    \n                $additionalInfoDictionary[\"BelowMemoryThreshold\"] = \"false:needs$($result.PMemoryPercentage)-max$memoryThreshold\"                    \n            }\n            $hasCpuRamPerfMetrics = $true\n        }\n        else {\n            $fitScore -= 0.5\n            $additionalInfoDictionary[\"BelowMemoryThreshold\"] = \"unknown:max$memoryThreshold\"\n        }\n        if (-not([string]::isNullOrEmpty($result.PNetworkMbps))) {\n            if ([double]$result.PNetworkMbps -ge [double]$networkThreshold) {\n                $fitScore -= 0.1    \n                $additionalInfoDictionary[\"BelowNetworkThreshold\"] = \"false:needs$($result.PNetworkMbps)-max$networkThreshold\"                    \n            }\n        }\n        else {\n            $fitScore -= 0.1\n            $additionalInfoDictionary[\"BelowNetworkThreshold\"] = \"unknown:max$networkThreshold\"\n        }\n\n        $fitScore = [Math]::max(0.0, $fitScore)\n    }\n    else\n    {\n        if (-not([string]::IsNullOrEmpty($additionalInfoDictionary[\"annualSavingsAmount\"])))\n        {\n            $savingsMonthly = [double] $additionalInfoDictionary[\"annualSavingsAmount\"] / 12\n        }\n        else\n        {\n            if ($result.RecommendationTypeId_g -eq $rightSizeRecommendationId)\n            {\n                $savingsMonthly = [double] $result.Last30DaysCost \n            }\n            else\n            {\n                $savingsMonthly = 0.0 # unknown\n            }\n        }            \n    }\n\n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $savingsMonthly     \n\n    $queryInstanceId = $result.InstanceId_s\n    if (-not($hasCpuRamPerfMetrics))\n    {\n        switch ($result.Cloud_s)\n        {\n            \"AzureCloud\" { $azureTld = \"com\" }\n            \"AzureChinaCloud\" { $azureTld = \"cn\" }\n            \"AzureUSGovernment\" { $azureTld = \"us\" }\n            default { $azureTld = \"com\" }\n        }\n        \n        $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n    }\n    else\n    {\n        $queryWorkspace = \"\"\n        if (-not([string]::IsNullOrEmpty($result.WorkspaceId)) -and $result.WorkspaceId -ne $workspaceId)\n        {\n            $queryWorkspace = \"workspace('$($result.WorkspaceId)').\"\n        }\n\n        $queryText = @\"\n        let perfInterval = $($perfDaysBackwards)d;\n        let armId = tolower(`'$queryInstanceId`');\n        let gInt = $perfTimeGrain;\n        let LinuxMemoryPerf = $($queryWorkspace)Perf \n        | where TimeGenerated > ago(perfInterval) \n        | where CounterName == '% Used Memory' and _ResourceId =~ armId\n        | project TimeGenerated, MemoryPercentage = CounterValue; \n        let WindowsMemoryPerf = $($queryWorkspace)Perf \n        | where TimeGenerated > ago(perfInterval) \n        | where CounterName == 'Available MBytes' and _ResourceId =~ armId\n        | extend MemoryAvailableMBs = CounterValue, InstanceId = tolower(_ResourceId) \n        | project TimeGenerated, MemoryAvailableMBs, InstanceId;\n        let MemoryPerf = WindowsMemoryPerf\n        | join kind=inner (\n            $vmsTableName \n            | where TimeGenerated > ago(1d)\n            | extend InstanceId = tolower(InstanceId_s)\n            | distinct InstanceId, MemoryMB_s\n        ) on InstanceId\n        | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 \n        | project TimeGenerated, MemoryPercentage\n        | union LinuxMemoryPerf\n        | summarize P$($memoryPercentile)MemoryPercentage = percentile(MemoryPercentage, $memoryPercentile) by bin(TimeGenerated, gInt);\n        let ProcessorPerf = $($queryWorkspace)Perf \n        | where TimeGenerated > ago(perfInterval) \n        | where CounterName == '% Processor Time' and InstanceName == '_Total' and _ResourceId =~ armId\n        | summarize P$($cpuPercentile)CPUPercentage = percentile(CounterValue, $cpuPercentile) by bin(TimeGenerated, gInt);\n        MemoryPerf\n        | join kind=inner (ProcessorPerf) on TimeGenerated\n        | render timechart\n\"@\n\n        switch ($cloudEnvironment)\n        {\n            \"AzureCloud\" { $azureTld = \"com\" }\n            \"AzureChinaCloud\" { $azureTld = \"cn\" }\n            \"AzureUSGovernment\" { $azureTld = \"us\" }\n            default { $azureTld = \"com\" }\n        }\n\n        $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n        $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n        $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n        $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"            \n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp                   = $timestamp\n        Cloud                       = $result.Cloud_s\n        Category                    = \"Cost\"\n        ImpactedArea                = $result.ImpactedArea_s\n        Impact                      = $result.Impact_s\n        RecommendationType          = \"Saving\"\n        RecommendationSubType       = \"AdvisorCost\"\n        RecommendationSubTypeId     = $result.RecommendationTypeId_g\n        RecommendationDescription   = $result.Description_s\n        RecommendationAction        = $result.RecommendationText_s\n        InstanceId                  = $result.InstanceId_s\n        InstanceName                = $result.InstanceName_s\n        AdditionalInfo              = $additionalInfoDictionary\n        ResourceGroup               = $result.ResourceGroup\n        SubscriptionGuid            = $result.SubscriptionGuid_g\n        SubscriptionName            = $result.SubscriptionName\n        TenantGuid                  = $result.TenantGuid_g\n        FitScore                    = $fitScore\n        Tags                        = $tags\n        DetailsURL                  = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\nWrite-Output \"Exporting final $($recommendations.Count) results as a JSON file...\"\n\n$fileDate = $datetime.ToString(\"yyyyMMdd\")\n$jsonExportPath = \"advisor-cost-augmented-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\nWrite-Output \"Uploading $jsonExportPath to blob storage...\"\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\" };\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$perfDaysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($perfDaysBackwards -gt 0)) {\n    $perfDaysBackwards = 7\n}\n\n$perfTimeGrain = Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfTimeGrain\" -ErrorAction SilentlyContinue\nif (-not($perfTimeGrain)) {\n    $perfTimeGrain = \"1h\"\n}\n\n# percentiles variables\n$cpuPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileCpu\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentile -gt 0)) {\n    $cpuPercentile = 99\n}\n$memoryPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileMemory\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentile -gt 0)) {\n    $memoryPercentile = 99\n}\n\n# perf thresholds variables\n$cpuPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentageThreshold -gt 0)) {\n    $cpuPercentageThreshold = 30\n}\n$memoryPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdMemoryPercentage\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentageThreshold -gt 0)) {\n    $memoryPercentageThreshold = 50\n}\n$cpuDegradedMaxPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuDegradedMaxPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuDegradedMaxPercentageThreshold -gt 0)) {\n    $cpuDegradedMaxPercentageThreshold = 95\n}\n$cpuDegradedAvgPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuDegradedAvgPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuDegradedAvgPercentageThreshold -gt 0)) {\n    $cpuDegradedAvgPercentageThreshold = 75\n}\n$memoryDegradedPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdMemoryDegradedPercentage\" -ErrorAction SilentlyContinue)\nif (-not($memoryDegradedPercentageThreshold -gt 0)) {\n    $memoryDegradedPercentageThreshold = 90\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('AppServicePlans','MonitorMetrics','AzureConsumption','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$appServicePlansTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AppServicePlans' }).LogAnalyticsSuffix + \"_CL\"\n$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $appServicePlansTableName, $subscriptionsTableName, $metricsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\n# Execute the recommendation query against Log Analytics\nWrite-Output \"Looking for underused App Service Plans, with less than $cpuPercentageThreshold% CPU and $memoryPercentageThreshold% RAM usage...\"\n\n$baseQuery = @\"\n    let billingInterval = 30d; \n    let perfInterval = $($perfDaysBackwards)d; \n    let cpuPercentileValue = $cpuPercentile;\n    let memoryPercentileValue = $memoryPercentile;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); \n    let stime = etime-billingInterval; \n\n    let BilledPlans = $consumptionTableName \n    | where todatetime(Date_s) between (stime..etime) and ResourceId has 'microsoft.web/serverfarms'\n    | extend ConsumedQuantity = todouble(Quantity_s)\n    | extend FinalCost = todouble(EffectivePrice_s) * ConsumedQuantity\n    | extend InstanceId_s = tolower(ResourceId)\n    | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(ConsumedQuantity) by InstanceId_s;\n\n    let ProcessorPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where ResourceId has 'microsoft.web/serverfarms'\n    | where MetricNames_s == \"CpuPercentage\" and AggregationType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PCPUPercentage = percentile(todouble(MetricValue_s), cpuPercentileValue) by InstanceId_s;\n\n    let MemoryPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where ResourceId has 'microsoft.web/serverfarms'\n    | where MetricNames_s == \"MemoryPercentage\" and AggregationType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PMemoryPercentage = percentile(todouble(MetricValue_s), memoryPercentileValue) by InstanceId_s;\n    \n    $appServicePlansTableName \n    | where TimeGenerated > ago(1d) and ComputeMode_s == 'Dedicated' and SkuTier_s != 'Free'\n    | distinct InstanceId_s, AppServicePlanName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, Tags_s\n    | join kind=inner ( BilledPlans ) on InstanceId_s \n    | join kind=leftouter ( MemoryPerf ) on InstanceId_s\n    | join kind=leftouter ( ProcessorPerf ) on InstanceId_s\n    | project InstanceId_s, AppServicePlan = AppServicePlanName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, PMemoryPercentage, PCPUPercentage, Tags_s, Last30DaysCost, Last30DaysQuantity\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionId\n    | where isnotempty(PMemoryPercentage) and isnotempty(PCPUPercentage) and PMemoryPercentage < $memoryPercentageThreshold and PCPUPercentage < $cpuPercentageThreshold\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\nlet perfInterval = $($perfDaysBackwards)d; \nlet armId = `'$queryInstanceId`';\nlet gInt = $perfTimeGrain;\nlet MemoryPerf = $metricsTableName \n| where TimeGenerated > ago(perfInterval) \n| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n| where ResourceId == armId\n| where MetricNames_s == 'MemoryPercentage' and AggregationType_s == 'Maximum'\n| extend MemoryPercentage = todouble(MetricValue_s)\n| summarize percentile(MemoryPercentage, $memoryPercentile) by bin(CollectedDate, gInt);\nlet ProcessorPerf = $metricsTableName \n| where TimeGenerated > ago(perfInterval) \n| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n| where ResourceId == armId\n| where MetricNames_s == 'CpuPercentage' and AggregationType_s == 'Maximum'\n| extend ProcessorPercentage = todouble(MetricValue_s)\n| summarize percentile(ProcessorPercentage, $cpuPercentile) by bin(CollectedDate, gInt);\nMemoryPerf\n| join kind=inner (ProcessorPerf) on CollectedDate\n| render timechart\n\"@\n\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = \"$($result.SkuSize_s)\"\n    $additionalInfoDictionary[\"InstanceCount\"] = [int] $result.NumberOfWorkers_s\n    $additionalInfoDictionary[\"MetricCPUPercentage\"] = \"$($result.PCPUPercentage)\"\n    $additionalInfoDictionary[\"MetricMemoryPercentage\"] = \"$($result.PMemoryPercentage)\"\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = ([double] $result.Last30DaysCost / 2)\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Web/serverFarms\"\n        Impact = \"High\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"UnderusedAppServicePlans\"\n        RecommendationSubTypeId = \"042adaca-ebdf-49b4-bc1b-2800b6e40fea\"\n        RecommendationDescription = \"Underused App Service Plans (performance capacity waste)\"\n        RecommendationAction = \"Right-size underused App Service Plans or scale it in\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.AppServicePlan\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroup\n        SubscriptionGuid = $result.SubscriptionId\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"appserviceplans-underused-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for performance constrained App Service Plans, with more than $cpuDegradedMaxPercentageThreshold% Max. CPU, $cpuDegradedAvgPercentageThreshold% Avg. CPU and $memoryDegradedPercentageThreshold% RAM usage...\"\n\n$baseQuery = @\"\n    let perfInterval = $($perfDaysBackwards)d; \n\n    let MemoryPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where ResourceId has 'microsoft.web/serverfarms'\n    | where MetricNames_s == \"MemoryPercentage\" and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PMemoryPercentage = avg(todouble(MetricValue_s)) by InstanceId_s;\n    \n    let ProcessorMaxPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where ResourceId has 'microsoft.web/serverfarms'\n    | where MetricNames_s == \"CpuPercentage\" and AggregationType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PCPUMaxPercentage = avg(todouble(MetricValue_s)) by InstanceId_s;\n\n    let ProcessorAvgPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where ResourceId has 'microsoft.web/serverfarms'\n    | where MetricNames_s == \"CpuPercentage\" and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PCPUAvgPercentage = avg(todouble(MetricValue_s)) by InstanceId_s;\n\n    $appServicePlansTableName \n    | where TimeGenerated > ago(1d) and ComputeMode_s == 'Dedicated' and SkuTier_s != 'Free'\n    | distinct InstanceId_s, AppServicePlanName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, Tags_s\n    | join kind=leftouter ( MemoryPerf ) on InstanceId_s\n    | join kind=leftouter ( ProcessorMaxPerf ) on InstanceId_s\n    | join kind=leftouter ( ProcessorAvgPerf ) on InstanceId_s\n    | project InstanceId_s, AppServicePlan = AppServicePlanName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, SkuSize_s, NumberOfWorkers_s, PMemoryPercentage, PCPUMaxPercentage, PCPUAvgPercentage, Tags_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionId\n    | where isnotempty(PMemoryPercentage) and isnotempty(PCPUAvgPercentage) and isnotempty(PCPUMaxPercentage) and (PMemoryPercentage > $memoryDegradedPercentageThreshold or (PCPUMaxPercentage > $cpuDegradedMaxPercentageThreshold and PCPUAvgPercentage > $cpuDegradedAvgPercentageThreshold))\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\nlet perfInterval = $($perfDaysBackwards)d; \nlet armId = `'$queryInstanceId`';\nlet gInt = $perfTimeGrain;\nlet MemoryPerf = $metricsTableName \n| where TimeGenerated > ago(perfInterval) \n| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n| where ResourceId == armId\n| where MetricNames_s == 'MemoryPercentage' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n| extend MemoryPercentage = todouble(MetricValue_s)\n| summarize percentile(MemoryPercentage, $memoryPercentile) by bin(CollectedDate, gInt);\nlet ProcessorMaxPerf = $metricsTableName \n| where TimeGenerated > ago(perfInterval) \n| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n| where ResourceId == armId\n| where MetricNames_s == 'CpuPercentage' and AggregationType_s == 'Maximum'\n| extend ProcessorMaxPercentage = todouble(MetricValue_s)\n| summarize percentile(ProcessorMaxPercentage, $cpuPercentile) by bin(CollectedDate, gInt);\nlet ProcessorAvgPerf = $metricsTableName \n| where TimeGenerated > ago(perfInterval) \n| extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n| where ResourceId == armId\n| where MetricNames_s == 'CpuPercentage' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n| extend ProcessorAvgPercentage = todouble(MetricValue_s)\n| summarize percentile(ProcessorAvgPercentage, $cpuPercentile) by bin(CollectedDate, gInt);\nMemoryPerf\n| join kind=inner (ProcessorMaxPerf) on CollectedDate\n| join kind=inner (ProcessorAvgPerf) on CollectedDate\n| render timechart\n\"@\n\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = \"$($result.SkuSize_s)\"\n    $additionalInfoDictionary[\"InstanceCount\"] = [int] $result.NumberOfWorkers_s\n    $additionalInfoDictionary[\"MetricCPUAvgPercentage\"] = \"$($result.PCPUAvgPercentage)\"\n    $additionalInfoDictionary[\"MetricCPUMaxPercentage\"] = \"$($result.PCPUMaxPercentage)\"\n    $additionalInfoDictionary[\"MetricMemoryPercentage\"] = \"$($result.PMemoryPercentage)\"\n\n    $fitScore = 3 # needs a more complete analysis to improve score\n\n    if ([double] $result.PCPUMaxPercentage -gt [double] $cpuDegradedMaxPercentageThreshold -and [double] $result.PCPUAvgPercentage -gt [double] $cpuDegradedAvgPercentageThreshold)\n    {\n        $fitScore = 4\n    }\n    \n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Performance\"\n        ImpactedArea = \"Microsoft.Web/serverFarms\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"PerfConstrainedAppServicePlans\"\n        RecommendationSubTypeId = \"351574cb-c105-4538-a778-11dfbe4857bf\"\n        RecommendationDescription = \"App Service Plan performance has been constrained by lack of resources\"\n        RecommendationAction = \"Resize App Service Plan to higher SKU or scale it out\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.AppServicePlan\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroup\n        SubscriptionGuid = $result.SubscriptionId\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"appserviceplans-perfconstrained-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for empty App Service Plans...\"\n\n$baseQuery = @\"\nlet interval = 30d;\nlet etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); \nlet stime = etime-interval; \n$appServicePlansTableName\n| where TimeGenerated > ago(1d) and ComputeMode_s == 'Dedicated' and SkuTier_s != 'Free' and toint(NumberOfSites_s) == 0\n| distinct AppServicePlanName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuSize_s, NumberOfWorkers_s, Tags_s, Cloud_s \n| join kind=leftouter (\n    $consumptionTableName\n    | where todatetime(Date_s) between (stime..etime)\n    | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n) on InstanceId_s\n| summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by AppServicePlanName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuSize_s, NumberOfWorkers_s, Tags_s, Cloud_s\n| join kind=leftouter ( \n    $subscriptionsTableName\n    | where TimeGenerated > ago(1d) \n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n    $appServicePlansTableName\n    | where InstanceId_s == '$queryInstanceId'\n    | where toint(NumberOfSites_s) == 0\n    | distinct InstanceId_s, AppServicePlanName_s, TimeGenerated\n    | summarize FirstUnusedDate = min(TimeGenerated) by InstanceId_s, AppServicePlanName_s\n    | join kind=leftouter (\n        $consumptionTableName\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize CostsSinceUnused = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > FirstUnusedDate) by AppServicePlanName_s, FirstUnusedDate\n\"@\n\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = $result.SkuSize_s\n    $additionalInfoDictionary[\"InstanceCount\"] = $result.NumberOfWorkers_s\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.Last30DaysCost \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Web/serverFarms\"\n        Impact = \"High\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"EmptyAppServicePlans\"\n        RecommendationSubTypeId = \"ef525225-8b91-47a3-81f3-e674e94564b6\"\n        RecommendationDescription = \"App Service Plans without any application incur in unnecessary costs\"\n        RecommendationAction = \"Delete the App Service Plan\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.AppServicePlanName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"appserviceplans-empty-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-DiskMonthlyPrice {\n    param (\n        [object[]] $SKUPriceSheet,\n        [string] $DiskSizeTier\n    )\n\n    $diskSkus = $SKUPriceSheet | Where-Object { $_.MeterName_s.Replace(\" Disks\",\"\") -eq $DiskSizeTier }\n    $targetMonthlyPrice = [double]::MaxValue\n    if ($diskSkus)\n    {\n        $targetMonthlyPrice = [double] ($diskSkus | Sort-Object -Property UnitPrice_s | Select-Object -First 1).UnitPrice_s\n    }\n    return $targetMonthlyPrice\n}\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n# perf thresholds variables\n$iopsPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdDiskIOPSPercentage\" -ErrorAction SilentlyContinue)\nif (-not($iopsPercentageThreshold -gt 0)) {\n    $iopsPercentageThreshold = 5\n}\n$mbsPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdDiskMBsPercentage\" -ErrorAction SilentlyContinue)\nif (-not($mbsPercentageThreshold -gt 0)) {\n    $mbsPercentageThreshold = 5\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$perfDaysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($perfDaysBackwards -gt 0)) {\n    $perfDaysBackwards = 7\n}\n\n$perfTimeGrain = Get-AutomationVariable -Name \"AzureOptimization_RecommendPerfTimeGrain\" -ErrorAction SilentlyContinue\nif (-not($perfTimeGrain)) {\n    $perfTimeGrain = \"1h\"\n}\n\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\"\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGManagedDisk','MonitorMetrics','ARGResourceContainers','AzureConsumption','Pricesheet')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$disksTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGManagedDisk' }).LogAnalyticsSuffix + \"_CL\"\n$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$pricesheetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'Pricesheet' }).LogAnalyticsSuffix + \"_CL\"\n\n\nWrite-Output \"Will run query against tables $disksTableName, $metricsTableName, $subscriptionsTableName, $pricesheetTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\nWrite-Output \"Getting Disks SKUs for the $referenceRegion region...\"\n\n$skus = Get-AzComputeResourceSku -Location $referenceRegion | Where-Object { $_.ResourceType -eq \"disks\" }\n\nWrite-Output \"Getting the current Pricesheet...\"\n\nif ($cloudEnvironment -eq \"AzureCloud\")\n{\n    $pricesheetRegion = \"EU West\"\n}\n\ntry \n{\n    $pricesheetEntries = @()\n\n    $baseQuery = @\"\n    $pricesheetTableName\n    | where TimeGenerated > ago(14d)\n    | where MeterCategory_s == 'Storage' and MeterSubCategory_s endswith \"Managed Disks\" and MeterName_s endswith \"Disks\" and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption'\n    | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s\n\"@    \n\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 14) -Wait 600 -IncludeStatistics\n    $pricesheetEntries = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    \n    Write-Output \"Query finished with $($pricesheetEntries.Count) results.\"   \n    Write-Output \"Query statistics: $($queryResults.Statistics.query)\"    \n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    Write-Output \"Consumption pricesheet not available, will estimate savings based in price difference ratio...\"\n}\n\n$skuPricesFound = @{}\n\nWrite-Output \"Looking for underutilized Disks, with less than $iopsPercentageThreshold% IOPS and $mbsPercentageThreshold% MB/s usage...\"\n\n$baseQuery = @\"\n    let billingInterval = 30d;\n    let perfInterval = $($perfDaysBackwards)d; \n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(billingInterval) | summarize max(todatetime(Date_s)))); \n    let stime = etime-billingInterval; \n\n    let BilledDisks = $consumptionTableName\n    | where todatetime(Date_s) between (stime..etime) and ResourceId contains '/disks/' and MeterCategory_s == 'Storage' and MeterSubCategory_s has 'Premium' and MeterName_s has 'Disk'\n    | extend DiskConsumedQuantity = todouble(Quantity_s)\n    | extend DiskPrice = todouble(EffectivePrice_s)\n    | extend FinalCost = DiskPrice * DiskConsumedQuantity\n    | extend ResourceId = tolower(ResourceId)\n    | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(DiskConsumedQuantity) by ResourceId;\n\n    $metricsTableName\n    | where MetricNames_s == 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec' and TimeGenerated > ago(perfInterval) and isnotempty(MetricValue_s)\n    | summarize MaxIOPSMetric = max(todouble(MetricValue_s)) by ResourceId\n    | join kind=inner ( \n        $disksTableName\n        | where TimeGenerated > ago(1d) and DiskState_s != 'Unattached' and SKU_s startswith 'Premium'\n        | project ResourceId=InstanceId_s, DiskName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, Tags_s, MaxIOPSDisk=toint(DiskIOPS_s), DiskSizeGB_s, SKU_s, DiskTier_s, DiskType_s\n    ) on ResourceId\n    | project-away ResourceId1\n    | extend IOPSPercentage = MaxIOPSMetric/MaxIOPSDisk*100\n    | where IOPSPercentage < $iopsPercentageThreshold\n    | join kind=inner (\n        $metricsTableName\n        | where MetricNames_s == 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec' and TimeGenerated > ago(perfInterval) and isnotempty(MetricValue_s)\n        | summarize MaxMBsMetric = max(todouble(MetricValue_s)/1024/1024) by ResourceId\n        | join kind=inner ( \n            $disksTableName\n            | where TimeGenerated > ago(1d) and DiskState_s != 'Unattached' and SKU_s startswith 'Premium'\n            | project ResourceId=InstanceId_s, DiskName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, Tags_s, MaxMBsDisk=toint(DiskThroughput_s), DiskSizeGB_s, SKU_s, DiskTier_s, DiskType_s\n        ) on ResourceId\n        | project-away ResourceId1\n        | extend MBsPercentage = MaxMBsMetric/MaxMBsDisk*100\n        | where MBsPercentage < $mbsPercentageThreshold\n    ) on ResourceId\n    | join kind=inner ( BilledDisks ) on ResourceId\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionId\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    throw \"Execution aborted\"\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $targetSku = $null\n    $currentDiskTier = $null\n\n    if ([string]::IsNullOrEmpty($result.DiskTier_s)) # older disks do not have Tier info in their properties\n    {\n        $currentSkuCandidates = @()\n        foreach ($sku in $skus)\n        {\n            $currentSkuCandidate = $null\n            $skuMinSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MinSizeGiB' }).Value\n            $skuMaxSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxSizeGiB' }).Value\n            $skuMaxIOps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxIOps' }).Value\n            $skuMaxBandwidthMBps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxBandwidthMBps' }).Value\n\n            if ($sku.Name -eq $result.SKU_s -and $skuMinSizeGB -lt [int]$result.DiskSizeGB_s -and $skuMaxSizeGB -ge [int]$result.DiskSizeGB_s `\n            -and [int]$skuMaxIOps -eq [int]$result.MaxIOPSDisk -and [int]$skuMaxBandwidthMBps -eq [int]$result.MaxMBsDisk)\n            {\n                if ($null -eq $skuPricesFound[$sku.Size])\n                {\n                    $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $sku.Size -SKUPriceSheet $pricesheetEntries\n                }\n    \n                $currentSkuCandidate = New-Object PSObject -Property @{\n                    Name = $sku.Size\n                    MaxSizeGB = $skuMaxSizeGB\n                }    \n\n                $currentSkuCandidates += $currentSkuCandidate    \n            }\n        }\n        $currentDiskTier = ($currentSkuCandidates | Sort-Object -Property MaxSizeGB | Select-Object -First 1).Name\n    }\n    else\n    {\n        $currentDiskTier = $result.DiskTier_s\n    }\n\n    if ($null -eq $skuPricesFound[$currentDiskTier])\n    {\n        $skuPricesFound[$currentDiskTier] = Find-DiskMonthlyPrice -DiskSizeTier $currentDiskTier -SKUPriceSheet $pricesheetEntries\n    }\n\n    $targetSkuPerfTier = $result.SKU_s.Replace(\"Premium\", \"StandardSSD\")\n    $targetSkuCandidates = @()\n\n    foreach ($sku in $skus)\n    {\n        $targetSkuCandidate = $null\n\n        $skuMinSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MinSizeGiB' }).Value\n        $skuMaxSizeGB = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxSizeGiB' }).Value\n        $skuMaxIOps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxIOps' }).Value\n        $skuMaxBandwidthMBps = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxBandwidthMBps' }).Value\n\n        if ($sku.Name -eq $targetSkuPerfTier -and $skuMinSizeGB -lt [int]$result.DiskSizeGB_s -and $skuMaxSizeGB -ge [int]$result.DiskSizeGB_s `\n                -and [double]$skuMaxIOps -ge [double]$result.MaxIOPSMetric -and [double]$skuMaxBandwidthMBps -ge [double]$result.MaxMBsMetric)\n        {\n            if ($null -eq $skuPricesFound[$sku.Size])\n            {\n                $skuPricesFound[$sku.Size] = Find-DiskMonthlyPrice -DiskSizeTier $sku.Size -SKUPriceSheet $pricesheetEntries\n            }\n\n            if ($skuPricesFound[$sku.Size] -lt [double]::MaxValue -and $skuPricesFound[$sku.Size] -lt $skuPricesFound[$currentDiskTier])\n            {\n                $targetSkuCandidate = New-Object PSObject -Property @{\n                    Name = $sku.Size\n                    MonthlyPrice = $skuPricesFound[$sku.Size]\n                    MaxSizeGB = $skuMaxSizeGB\n                    MaxIOPS = $skuMaxIOps\n                    MaxMBps = $skuMaxBandwidthMBps\n                }\n\n                $targetSkuCandidates += $targetSkuCandidate    \n            }\n        }\n    }\n\n    $targetSku = $targetSkuCandidates | Sort-Object -Property MonthlyPrice | Select-Object -First 1\n\n    if ($null -ne $targetSku)\n    {\n        $queryInstanceId = $result.ResourceId\n        $queryText = @\"\n        let billingInterval = 30d; \n        let armId = `'$queryInstanceId`';\n        let gInt = $perfTimeGrain;\n        let ThroughputMBsPerf = $metricsTableName \n        | where TimeGenerated > ago(billingInterval)\n        | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n        | where ResourceId == armId\n        | where MetricNames_s == 'Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n        | extend ThroughputMBs = todouble(MetricValue_s)/1024/1024\n        | project CollectedDate, ThroughputMBs, InstanceId_s=ResourceId\n        | join kind=inner (\n            $disksTableName \n            | where TimeGenerated > ago(1d)\n            | distinct InstanceId_s, DiskThroughput_s\n        ) on InstanceId_s\n        | extend MBsPercentage = ThroughputMBs / todouble(DiskThroughput_s) * 100 \n        | summarize max(MBsPercentage) by bin(CollectedDate, gInt);\n        let IOPSPerf = $metricsTableName  \n        | where TimeGenerated > ago(billingInterval) \n        | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n        | where ResourceId == armId\n        | where MetricNames_s == 'Composite Disk Read Operations/sec,Composite Disk Write Operations/sec' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n        | extend IOPS = todouble(MetricValue_s)\n        | project CollectedDate, IOPS, InstanceId_s=ResourceId\n        | join kind=inner (\n            $disksTableName  \n            | where TimeGenerated > ago(1d)\n            | distinct InstanceId_s, DiskIOPS_s\n        ) on InstanceId_s\n        | extend IOPSPercentage = IOPS / todouble(DiskIOPS_s) * 100 \n        | summarize max(IOPSPercentage) by bin(CollectedDate, gInt);\n        ThroughputMBsPerf\n        | join kind=inner (IOPSPerf) on CollectedDate\n        | render timechart\n\"@\n\n        switch ($cloudEnvironment)\n        {\n            \"AzureCloud\" { $azureTld = \"com\" }\n            \"AzureChinaCloud\" { $azureTld = \"cn\" }\n            \"AzureUSGovernment\" { $azureTld = \"us\" }\n            default { $azureTld = \"com\" }\n        }\n\n        $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n        $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n        $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n        $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n    \n        $additionalInfoDictionary = @{}\n    \n        $additionalInfoDictionary[\"DiskType\"] = \"Managed\"\n        $additionalInfoDictionary[\"currentSku\"] = $result.SKU_s\n        $additionalInfoDictionary[\"targetSku\"] = $targetSkuPerfTier\n        $additionalInfoDictionary[\"DiskSizeGB\"] = [int] $result.DiskSizeGB_s \n        $additionalInfoDictionary[\"currentTier\"] = $currentDiskTier \n        $additionalInfoDictionary[\"targetTier\"] = $targetSku.Name \n        $additionalInfoDictionary[\"MaxIOPSMetric\"] = [double] $($result.MaxIOPSMetric)\n        $additionalInfoDictionary[\"MaxMBpsMetric\"] = [double] $($result.MaxMBsMetric)\n        $additionalInfoDictionary[\"MetricIOPSPercentage\"] = [double] $($result.IOPSPercentage)\n        $additionalInfoDictionary[\"MetricMBpsPercentage\"] = [double] $($result.MBsPercentage)\n        $additionalInfoDictionary[\"targetMaxSizeGB\"] = [int] $targetSku.MaxSizeGB \n        $additionalInfoDictionary[\"targetMaxIOPS\"] = [int] $targetSku.MaxIOPS \n        $additionalInfoDictionary[\"targetMaxMBps\"] =[int] $targetSku.MaxMBps \n    \n        $fitScore = 4 # needs Maximum of Maximum for metrics to have higher fit score\n        if ([int] $result.DiskSizeGB_s -gt 512)\n        {\n            $fitScore = 3.5 #disk will not support credit-based bursting, therefore the recommendation risk increases a bit\n        }\n        \n        $fitScore = [Math]::max(0.0, $fitScore)\n\n        $savingCoefficient = 2 # Standard SSD is generally close to half the price of Premium SSD\n\n        $targetSkuSavingsMonthly = $result.Last30DaysCost - ($result.Last30DaysCost / $savingCoefficient)\n\n        $tentativeTargetSkuSavingsMonthly = -1\n\n        if ($targetSku -and $skuPricesFound[$targetSku.Name] -lt [double]::MaxValue)\n        {\n            $targetSkuPrice = $skuPricesFound[$targetSku.Name]    \n\n            if ($skuPricesFound[$currentDiskTier] -lt [double]::MaxValue)\n            {\n                $currentSkuPrice = $skuPricesFound[$currentDiskTier]    \n                $tentativeTargetSkuSavingsMonthly = ($currentSkuPrice * [double] $result.Last30DaysQuantity) - ($targetSkuPrice * [double] $result.Last30DaysQuantity)    \n            }\n            else\n            {\n                $tentativeTargetSkuSavingsMonthly = $result.Last30DaysCost - ($targetSkuPrice * [double] $result.Last30DaysQuantity)    \n            }\n        }\n\n        if ($tentativeTargetSkuSavingsMonthly -ge 0)\n        {\n            $targetSkuSavingsMonthly = $tentativeTargetSkuSavingsMonthly\n        }\n\n        $tags = @{}\n\n        if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n        {\n            $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n            foreach ($tagPairString in $tagPairs)\n            {\n                $tagPair = $tagPairString.Split('=')\n                if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n                {\n                    $tagName = $tagPair[0].Trim()\n                    $tagValue = $tagPair[1].Trim()\n                    $tags[$tagName] = $tagValue    \n                }\n            }\n        }            \n    \n        if ($targetSkuSavingsMonthly -eq [double]::PositiveInfinity)\n        {\n            $targetSkuSavingsMonthly = [double] $result.Last30DaysCost / 2\n        }\n\n        $additionalInfoDictionary[\"savingsAmount\"] = [double] $targetSkuSavingsMonthly     \n        $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    \n        $recommendation = New-Object PSObject -Property @{\n            Timestamp                   = $timestamp\n            Cloud                       = $result.Cloud_s\n            Category                    = \"Cost\"\n            ImpactedArea                = \"Microsoft.Compute/disks\"\n            Impact                      = \"High\"\n            RecommendationType          = \"Saving\"\n            RecommendationSubType       = \"UnderusedPremiumSSDDisks\"\n            RecommendationSubTypeId     = \"4854b5dc-4124-4ade-879e-6a7bb65350ab\"\n            RecommendationDescription   = \"Premium SSD disk has been underutilized\"\n            RecommendationAction        = \"Change disk tier at least to the equivalent for Standard SSD\"\n            InstanceId                  = $result.ResourceId\n            InstanceName                = $result.DiskName_s\n            AdditionalInfo              = $additionalInfoDictionary\n            ResourceGroup               = $result.ResourceGroup\n            SubscriptionGuid            = $result.SubscriptionId\n            SubscriptionName            = $result.SubscriptionName\n            TenantGuid                  = $result.TenantGuid_g\n            FitScore                    = $fitScore\n            Tags                        = $tags\n            DetailsURL                  = $detailsURL\n        }\n    \n        $recommendations += $recommendation        \n    }\n}\n\n# Export the recommendations as JSON to blob storage\n\nWrite-Output \"Exporting final $($recommendations.Count) results as a JSON file...\"\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"disks-underutilized-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$perfDaysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($perfDaysBackwards -gt 0)) {\n    $perfDaysBackwards = 7\n}\n\n$dtuPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileSqlDtu\" -ErrorAction SilentlyContinue)\nif (-not($dtuPercentile -gt 0)) {\n    $dtuPercentile = 99\n}\n$dtuPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdDtuPercentage\" -ErrorAction SilentlyContinue)\nif (-not($dtuPercentageThreshold -gt 0)) {\n    $dtuPercentageThreshold = 40\n}\n$dtuDegradedPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdDtuDegradedPercentage\" -ErrorAction SilentlyContinue)\nif (-not($dtuDegradedPercentageThreshold -gt 0)) {\n    $dtuDegradedPercentageThreshold = 75\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGSqlDb','MonitorMetrics','AzureConsumption','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$sqlDbsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGSqlDb' }).LogAnalyticsSuffix + \"_CL\"\n$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $sqlDbsTableName, $subscriptionsTableName, $metricsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\n# Execute the recommendation query against Log Analytics\nWrite-Output \"Looking for underused SQL Databases, with less than $dtuPercentageThreshold % Max. DTU usage...\"\n\n$baseQuery = @\"\n    let DTUPercentageThreshold = $dtuPercentageThreshold;\n    let MetricsInterval = $($perfDaysBackwards)d;\n    let BillingInterval = 30d;\n    let dtuPercentPercentile = $dtuPercentile;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(BillingInterval) | summarize max(todatetime(Date_s)))); \n    let stime = etime-BillingInterval; \n    let CandidateDatabaseIds = $sqlDbsTableName\n    | where TimeGenerated > ago(1d) and SkuName_s in ('Standard','Premium')\n    | distinct InstanceId_s;\n    $metricsTableName\n    | where TimeGenerated > ago(MetricsInterval)\n    | where ResourceId in (CandidateDatabaseIds) and MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Maximum'\n    | summarize P99DTUPercentage = percentile(todouble(MetricValue_s), dtuPercentPercentile) by ResourceId\n    | where P99DTUPercentage < DTUPercentageThreshold\n    | join (\n        $sqlDbsTableName\n        | where TimeGenerated > ago(1d)\n        | project ResourceId = InstanceId_s, DBName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s\n    ) on ResourceId\n    | join kind=leftouter (\n        $consumptionTableName\n        | where todatetime(Date_s) between (stime..etime)\n        | project ResourceId=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on ResourceId\n    | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by DBName_s, ResourceId, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s, P99DTUPercentage\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.ResourceId\n    $queryText = @\"\n    $metricsTableName\n    | where ResourceId == '$queryInstanceId'\n    | where MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Maximum'\n    | project TimeGenerated, DTUPercentage = toint(MetricValue_s)\n    | render timechart\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = \"$($result.SkuName_s) $($result.ServiceObjectiveName_s)\"\n    $additionalInfoDictionary[\"DTUPercentage\"] = [int] $result.P99DTUPercentage \n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = ([double] $result.Last30DaysCost / 2)\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Sql/servers/databases\"\n        Impact = \"High\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"UnderusedSqlDatabases\"\n        RecommendationSubTypeId = \"ff68f4e5-1197-4be9-8e5f-8760d7863cb4\"\n        RecommendationDescription = \"Underused SQL Databases (performance capacity waste)\"\n        RecommendationAction = \"Right-size underused SQL Databases\"\n        InstanceId = $result.ResourceId\n        InstanceName = $result.DBName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"sqldbs-underused-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for performance constrained SQL Databases, with more than $dtuDegradedPercentageThreshold % Avg. DTU usage...\"\n\n$baseQuery = @\"\n    let DTUPercentageThreshold = $dtuDegradedPercentageThreshold;\n    let MetricsInterval = $($perfDaysBackwards)d;\n    let CandidateDatabaseIds = $sqlDbsTableName\n    | where TimeGenerated > ago(1d) and SkuName_s in ('Basic','Standard','Premium')\n    | distinct InstanceId_s;\n    $metricsTableName\n    | where TimeGenerated > ago(MetricsInterval)\n    | where ResourceId in (CandidateDatabaseIds) and MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Average' and AggregationOfType_s == 'Maximum'\n    | summarize AvgDTUPercentage = avg(todouble(MetricValue_s)) by ResourceId\n    | where AvgDTUPercentage > DTUPercentageThreshold\n    | join (\n        $sqlDbsTableName\n        | where TimeGenerated > ago(1d)\n        | project ResourceId = InstanceId_s, DBName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s\n    ) on ResourceId\n    | project DBName_s, ResourceId, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, SkuName_s, ServiceObjectiveName_s, Tags_s, Cloud_s, AvgDTUPercentage\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.ResourceId\n    $queryText = @\"\n    $metricsTableName\n    | where ResourceId == '$queryInstanceId'\n    | where MetricNames_s == 'dtu_consumption_percent' and AggregationType_s == 'Average'\n    | project TimeGenerated, DTUPercentage = toint(MetricValue_s)\n    | render timechart\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = \"$($result.SkuName_s) $($result.ServiceObjectiveName_s)\"\n    $additionalInfoDictionary[\"DTUPercentage\"] = [int] $result.AvgDTUPercentage \n\n    $fitScore = 4\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Performance\"\n        ImpactedArea = \"Microsoft.Sql/servers/databases\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"PerfConstrainedSqlDatabases\"\n        RecommendationSubTypeId = \"724ff2f5-8c83-4105-b00d-029c4560d774\"\n        RecommendationDescription = \"SQL Database performance has been constrained by lack of resources\"\n        RecommendationAction = \"Resize SQL Database to higher SKU or scale it out\"\n        InstanceId = $result.ResourceId\n        InstanceName = $result.DBName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"sqldbs-perfconstrained-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1",
    "content": "function ConvertTo-Hashtable {\n    [CmdletBinding()]\n    [OutputType('hashtable')]\n    param (\n        [Parameter(ValueFromPipeline)]\n        $InputObject\n    )\n \n    process {\n        if ($null -eq $InputObject) {\n            return $null\n        }\n \n        if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {\n            $collection = @(\n                foreach ($object in $InputObject) {\n                    ConvertTo-Hashtable -InputObject $object\n                }\n            ) \n            Write-Output -NoEnumerate $collection\n        } elseif ($InputObject -is [psobject]) { \n            $hash = @{}\n            foreach ($property in $InputObject.PSObject.Properties) {\n                $hash[$property.Name] = ConvertTo-Hashtable -InputObject $property.Value\n            }\n            $hash\n        } else {\n            $InputObject\n        }\n    }\n}\n\n$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n# storage account thresholds variables\n$growthPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage\" -ErrorAction SilentlyContinue)\nif (-not($growthPercentageThreshold -gt 0)) {\n    $growthPercentageThreshold = 5\n}\n$monthlyCostThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold\" -ErrorAction SilentlyContinue)\nif (-not($monthlyCostThreshold -gt 0)) {\n    $monthlyCostThreshold = 50\n}\n$growthLookbackDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationStorageAcountGrowthLookbackDays\" -ErrorAction SilentlyContinue)\nif (-not($growthLookbackDays -gt 0)) {\n    $growthLookbackDays = 30\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n$tenantId = (Get-AzContext).Tenant.Id\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGResourceContainers','AzureConsumption')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $subscriptionsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = $growthLookbackDays + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\nWrite-Output \"Looking for ever growing Storage Accounts, with more than $monthlyCostThreshold/month costs, growing more than $growthPercentageThreshold% over the last $growthLookbackDays days...\"\n\n$dailyCostThreshold = [Math]::Round($monthlyCostThreshold / 30)\n\n$baseQuery = @\"\nlet interval = $($growthLookbackDays)d;\nlet etime = endofday(todatetime(toscalar($consumptionTableName | where todatetime(Date_s) > ago(interval) and todatetime(Date_s) < now() | summarize max(todatetime(Date_s)))));\nlet etime_subs = endofday(todatetime(toscalar($subscriptionsTableName | where TimeGenerated > ago(interval) | summarize max(TimeGenerated))));\nlet stime = endofday(etime-interval);\nlet lastday_stime = endofday(etime-1d);\nlet lastday_stime_subs = endofday(etime_subs-1d);\nlet costThreshold = $dailyCostThreshold;\nlet growthPercentageThreshold = $growthPercentageThreshold; \nlet StorageAccountsWithLastTags = $consumptionTableName\n| where todatetime(Date_s) between (lastday_stime..etime)\n| where MeterCategory_s == 'Storage' and ConsumedService_s == 'Microsoft.Storage' and MeterName_s endswith 'Data Stored' and ChargeType_s == 'Usage'\n| extend ResourceId = tolower(ResourceId)\n| distinct ResourceId, Tags_s;\n$consumptionTableName\n| where todatetime(Date_s) between (stime..etime)\n| where MeterCategory_s == 'Storage' and ConsumedService_s == 'Microsoft.Storage' and MeterName_s endswith 'Data Stored' and ChargeType_s == 'Usage'\n| extend ResourceId = tolower(ResourceId)\n| make-series CostSum=sum(todouble(CostInBillingCurrency_s)) default=0.0 on todatetime(Date_s) from stime to etime step 1d by ResourceId, ResourceGroup, SubscriptionId\n| extend InitialDailyCost = todouble(CostSum[0]), CurrentDailyCost = todouble(CostSum[array_length(CostSum)-1])\n| extend GrowthPercentage = round((CurrentDailyCost-InitialDailyCost)/InitialDailyCost*100)\n| where InitialDailyCost > 0 and CurrentDailyCost > costThreshold and GrowthPercentage > growthPercentageThreshold \n| project ResourceId, InitialDailyCost, CurrentDailyCost, GrowthPercentage, ResourceGroup, SubscriptionId\n| join kind=leftouter (StorageAccountsWithLastTags) on ResourceId\n| join kind=leftouter ( \n    $subscriptionsTableName\n    | where TimeGenerated > lastday_stime_subs\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n    | project SubscriptionId=SubscriptionGuid_g, SubscriptionName = ContainerName_s \n) on SubscriptionId\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    throw \"Execution aborted\"\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.ResourceId\n    $queryText = @\"\n    $consumptionTableName \n    | where MeterCategory_s == 'Storage' and ConsumedService_s == 'Microsoft.Storage' and MeterName_s endswith 'Data Stored' and ChargeType_s == 'Usage'\n    | extend ResourceId = tolower(ResourceId)\n    | where ResourceId =~ '$queryInstanceId' \n    | summarize DailyCosts = sum(todouble(CostInBillingCurrency_s)) by bin(todatetime(Date_s), 1d)\n    | render timechart\n\"@\n\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $datetime.AddDays(-1 * $recommendationSearchTimeSpan).ToString(\"yyyy-MM-dd\")\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $costsAmount = ([double] $result.InitialDailyCost + [double] $result.CurrentDailyCost) / 2 * 30\n\n    $additionalInfoDictionary[\"InitialDailyCost\"] = $result.InitialDailyCost\n    $additionalInfoDictionary[\"CurrentDailyCost\"] = $result.CurrentDailyCost\n    $additionalInfoDictionary[\"GrowthPercentage\"] = $result.GrowthPercentage\n    $additionalInfoDictionary[\"CostsAmount\"] = $costsAmount\n    $additionalInfoDictionary[\"savingsAmount\"] = $costsAmount * 0.25 # estimated 25% savings\n\n    $fitScore = 4 # savings are estimated with a significant error margin\n    \n    $fitScore = [Math]::max(0.0, $fitScore)\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        if (-not($result.Tags_s -like \"{*\"))\n        {\n            $result.Tags_s = '{' + $result.Tags_s + '}'\n        }\n        $tags = ConvertFrom-Json $result.Tags_s | ConvertTo-Hashtable\n    }            \n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp                   = $timestamp\n        Cloud                       = $cloudEnvironment\n        Category                    = \"Cost\"\n        ImpactedArea                = \"Microsoft.Storage/storageAccounts\"\n        Impact                      = \"Medium\"\n        RecommendationType          = \"Saving\"\n        RecommendationSubType       = \"StorageAccountsGrowing\"\n        RecommendationSubTypeId     = \"08e049ca-18b0-4d22-b174-131a91d0381c\"\n        RecommendationDescription   = \"Storage Account without retention policy in place\"\n        RecommendationAction        = \"Review whether the Storage Account has a retention policy for example via Lifecycle Management\"\n        InstanceId                  = $result.ResourceId\n        InstanceName                = $result.ResourceId.Split('/')[-1]\n        AdditionalInfo              = $additionalInfoDictionary\n        ResourceGroup               = $result.ResourceGroup\n        SubscriptionGuid            = $result.SubscriptionId\n        SubscriptionName            = $result.SubscriptionName\n        TenantGuid                  = $tenantId\n        FitScore                    = $fitScore\n        Tags                        = $tags\n        DetailsURL                  = $detailsURL\n    }\n\n    $recommendations += $recommendation        \n}\n\n# Export the recommendations as JSON to blob storage\n\nWrite-Output \"Exporting final $($recommendations.Count) results as a JSON file...\"\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"storageaccounts-costsgrowing-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGManagedDisk','AzureConsumption','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$disksTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGManagedDisk' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $disksTableName, $subscriptionsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n# Execute the recommendation query against Log Analytics\n\n$baseQuery = @\"\n    let interval = 30d;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); \n    let stime = etime-interval;     \n    $disksTableName\n    | where TimeGenerated > ago(1d) and isempty(OwnerVMId_s) and Tags_s !has 'ASR-ReplicaDisk' and Tags_s !has 'asrseeddisk'\n    | distinct DiskName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SKU_s, DiskSizeGB_s, Tags_s, Cloud_s \n    | join kind=leftouter (\n        $consumptionTableName\n        | where todatetime(Date_s) between (stime..etime)\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by DiskName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SKU_s, DiskSizeGB_s, Tags_s, Cloud_s    \n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    throw \"Execution aborted\"\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n    $disksTableName\n    | where InstanceId_s == '$queryInstanceId' and isempty(OwnerVMId_s)\n    | distinct InstanceId_s, DiskName_s, DiskSizeGB_s, SKU_s, TimeGenerated\n    | summarize LastAttachedDate = min(TimeGenerated) by InstanceId_s, DiskName_s, DiskSizeGB_s, SKU_s\n    | join kind=leftouter (\n        $consumptionTableName\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize CostsSinceDetached = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > LastAttachedDate) by DiskName_s, LastAttachedDate, DiskSizeGB_s, SKU_s    \n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"DiskType\"] = \"Managed\"\n    $additionalInfoDictionary[\"currentSku\"] = $result.SKU_s\n    $additionalInfoDictionary[\"DiskSizeGB\"] = [int] $result.DiskSizeGB_s \n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.Last30DaysCost \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Compute/disks\"\n        Impact = \"Medium\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"UnattachedDisks\"\n        RecommendationSubTypeId = \"c84d5e86-e2d6-4d62-be7c-cecfbd73b0db\"\n        RecommendationDescription = \"Unattached disks (without owner VM) incur in unnecessary costs\"\n        RecommendationAction = \"Delete or downgrade disk to Standard SKU\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.DiskName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"unattacheddisks-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGAppGateway','AzureConsumption','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$appGWsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGAppGateway' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $appGWsTableName, $subscriptionsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n# Execute the Cost recommendation query against Log Analytics\n\n$baseQuery = @\"\n    let interval = 30d;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); \n    let stime = etime-interval; \n    $appGWsTableName\n    | where TimeGenerated > ago(1d)\n    | where toint(BackendPoolsCount_s) == 0 or ((BackendIPCount_s == 0 or isempty(BackendIPCount_s)) and (BackendAddressesCount_s == 0 or isempty(BackendAddressesCount_s)))\n    | distinct InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, SkuCapacity_s, Tags_s, Cloud_s \n    | join kind=leftouter (\n        $consumptionTableName\n        | where todatetime(Date_s) between (stime..etime)\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, SkuCapacity_s, Tags_s, Cloud_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    throw \"Execution aborted\"\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n    $appGWsTableName\n    | where InstanceId_s == '$queryInstanceId'\n    | where toint(BackendPoolsCount_s) == 0 or ((toint(BackendIPCount_s) == 0 or isempty(BackendIPCount_s)) and (toint(BackendAddressesCount_s) == 0 or isempty(BackendAddressesCount_s)))\n    | distinct InstanceId_s, InstanceName_s, TimeGenerated\n    | summarize FirstUnusedDate = min(TimeGenerated) by InstanceId_s, InstanceName_s\n    | join kind=leftouter (\n        $consumptionTableName\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize CostsSinceUnused = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > FirstUnusedDate) by InstanceName_s, FirstUnusedDate\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = $result.SkuName_s\n    $additionalInfoDictionary[\"InstanceCount\"] = $result.SkuCapacity_s\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.Last30DaysCost \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Network/applicationGateways\"\n        Impact = \"High\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"UnusedAppGateways\"\n        RecommendationSubTypeId = \"dc3d2baa-26c8-435e-aa9d-edb2bfd6fff6\"\n        RecommendationDescription = \"Application Gateways without a backend pool incur in unnecessary costs\"\n        RecommendationAction = \"Delete the Application Gateway\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.InstanceName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"unusedappgateways-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n"
  },
  {
    "path": "runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGLoadBalancer','AzureConsumption','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$lbsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGLoadBalancer' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $lbsTableName, $subscriptionsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\n# Execute the Cost recommendation query against Log Analytics\n\n$baseQuery = @\"\n    let interval = 30d;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(interval) | summarize max(todatetime(Date_s)))); \n    let stime = etime-interval; \n    $lbsTableName\n    | where TimeGenerated > ago(1d)\n    | where SkuName_s == 'Standard'\n    | where (toint(BackendPoolsCount_s) == 0 or ((toint(BackendIPCount_s) == 0 or isempty(BackendIPCount_s)) and (toint(BackendAddressesCount_s) == 0 or isempty(BackendAddressesCount_s)))) and toint(InboundNatPoolsCount_s) == 0\n    | where toint(LbRulesCount_s) != 0 or toint(InboundNatRulesCount_s) != 0 or toint(OutboundRulesCount_s) != 0\n    | distinct InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, Tags_s, Cloud_s \n    | join kind=leftouter (\n        $consumptionTableName\n        | where todatetime(Date_s) between (stime..etime)\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, Tags_s, Cloud_s    \n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Costs query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n    $lbsTableName\n    | where InstanceId_s == '$queryInstanceId'\n    | where SkuName_s == 'Standard'\n    | where (toint(BackendPoolsCount_s) == 0 or ((BackendIPCount_s == 0 or isempty(BackendIPCount_s)) and (BackendAddressesCount_s == 0 or isempty(BackendAddressesCount_s)))) and toint(InboundNatPoolsCount_s) == 0\n    | where toint(LbRulesCount_s) != 0 or toint(InboundNatRulesCount_s) != 0 or toint(OutboundRulesCount_s) != 0\n    | distinct InstanceId_s, InstanceName_s, SkuName_s, TimeGenerated\n    | summarize FirstUnusedDate = min(TimeGenerated) by InstanceId_s, InstanceName_s, SkuName_s\n    | join kind=leftouter (\n        $consumptionTableName\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize CostsSinceUnused = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > FirstUnusedDate) by InstanceName_s, FirstUnusedDate, SkuName_s\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = $result.SkuName_s\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.Last30DaysCost \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Network/loadBalancers\"\n        Impact = \"Medium\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"UnusedStandardLoadBalancers\"\n        RecommendationSubTypeId = \"f1ed3bb2-3cb5-41e6-ba38-7001d5ff87f5\"\n        RecommendationDescription = \"Standard Load Balancers with rules defined and without a backend pool incur in unnecessary costs\"\n        RecommendationAction = \"Delete the Load Balancer\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.InstanceName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"unusedstdloadbalancers-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\n\n# Execute the Operational Excellence recommendation query against Log Analytics\n\n$baseQuery = @\"\n    $lbsTableName\n    | where TimeGenerated > ago(1d)\n    | where (toint(BackendPoolsCount_s) == 0 or BackendIPCount_s == 0 or isempty(BackendIPCount_s)) and toint(InboundNatPoolsCount_s) == 0\n    | distinct InstanceName_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, Tags_s, Cloud_s \n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 2) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Operational Excellence query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$workspaceTenantId/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = $result.SkuName_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Network/loadBalancers\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"UnusedLoadBalancers\"\n        RecommendationSubTypeId = \"48619512-f4e6-4241-9c85-16f7c987950c\"\n        RecommendationDescription = \"Load Balancers without a backend pool are useless\"\n        RecommendationAction = \"Delete the Load Balancer\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.InstanceName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"unusedloadbalancers-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$deallocatedIntervalDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays\")\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGManagedDisk','ARGVirtualMachine','AzureConsumption','ARGResourceContainers')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$vmsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualMachine' }).LogAnalyticsSuffix + \"_CL\"\n$disksTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGManagedDisk' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $vmsTableName, $disksTableName, $subscriptionsTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = $deallocatedIntervalDays + $consumptionOffsetDaysStart\n$offlineInterval = $deallocatedIntervalDays + $consumptionOffsetDays\n$billingInterval = 30 + $consumptionOffsetDays\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\nWrite-Output \"Looking for VMs that have been deallocated for more than 30 days...\"\n\n# Execute the recommendation query against Log Analytics\n\n$baseQuery = @\"\n    let offlineInterval = $($offlineInterval)d;\n    let billingInterval = $($billingInterval)d;\n    let billingWindowIntervalEnd = $($consumptionOffsetDays)d; \n    let billingWindowIntervalStart = $($consumptionOffsetDaysStart)d; \n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(billingInterval) | summarize max(todatetime(Date_s)))); \n    let stime = etime-offlineInterval;\n    let BilledVMs = $consumptionTableName \n    | where todatetime(Date_s) between (stime..etime)\n    | where ResourceId like 'microsoft.compute/virtualmachines/' or ResourceId like 'microsoft.classiccompute/virtualmachines/' \n    | extend InstanceId_s = tolower(ResourceId)\n    | distinct InstanceId_s;\n    let RunningVMs = $vmsTableName\n    | where TimeGenerated > ago(billingWindowIntervalStart) and TimeGenerated < ago(billingWindowIntervalEnd)\n    | where PowerState_s has_any ('running','starting','readyrole')\n    | distinct InstanceId_s;\n    let BilledDisks = $consumptionTableName \n    | where todatetime(Date_s) between (stime..etime)\n    | where ResourceId like 'microsoft.compute/disks/'\n    | extend BillingInstanceId = tolower(ResourceId)\n    | summarize DisksCosts = sum(todouble(CostInBillingCurrency_s)) by BillingInstanceId;\n    $vmsTableName\n    | where TimeGenerated > ago(billingWindowIntervalStart) and TimeGenerated < ago(billingWindowIntervalEnd)\n    | where InstanceId_s !in (RunningVMs)\n    | join kind=leftouter (BilledVMs) on InstanceId_s\n    | where isempty(InstanceId_s1)\n    | project InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s \n    | join kind=leftouter (\n        $disksTableName \n        | where TimeGenerated > ago(1d)\n        | project DiskInstanceId = InstanceId_s, SKU_s, OwnerVMId_s\n    ) on `$left.InstanceId_s == `$right.OwnerVMId_s\n    | join kind=leftouter (\n        BilledDisks\n    ) on `$left.DiskInstanceId == `$right.BillingInstanceId\n    | summarize TotalDisksCosts = sum(DisksCosts) by InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n        let offlineInterval = $($offlineInterval)d;\n        $consumptionTableName\n        | extend ResourceId = tolower(ResourceId) \n        | where ResourceId =~ '$queryInstanceId'\n        | where todatetime(Date_s) < now()\n        | join kind=inner (\n            $disksTableName\n            | extend DiskInstanceId = InstanceId_s\n        )\n        on `$left.ResourceId == `$right.OwnerVMId_s\n        | summarize DeallocatedSince = max(todatetime(Date_s)) by DiskName_s, DiskSizeGB_s, SKU_s, DiskInstanceId \n        | join kind=inner\n        (\n            $consumptionTableName\n            | where todatetime(Date_s) > ago(offlineInterval)\n            | extend DiskInstanceId = tolower(ResourceId)\n            | summarize DiskCosts = sum(todouble(CostInBillingCurrency_s)) by DiskInstanceId\n        )\n        on DiskInstanceId\n        | project DeallocatedSince, DiskName_s, DiskSizeGB_s, SKU_s, MonthlyCosts = DiskCosts\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"LongDeallocatedThreshold\"] = $deallocatedIntervalDays\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.TotalDisksCosts \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.TotalDisksCosts \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"Medium\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"LongDeallocatedVms\"\n        RecommendationSubTypeId = \"c320b790-2e58-452a-aa63-7b62c383ad8a\"\n        RecommendationDescription = \"Virtual Machine has been deallocated for long with disks still incurring costs\"\n        RecommendationAction = \"Delete Virtual Machine or downgrade its disks to Standard HDD SKU\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"longdeallocatedvms-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMs that are stopped (not deallocated)...\"\n\n# Execute the recommendation query against Log Analytics\n\n$baseQuery = @\"\n    $vmsTableName\n    | where TimeGenerated > ago(1d)\n    | where PowerState_s has 'stopped'\n    | project InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s \n    | join kind=leftouter ( \n        $consumptionTableName\n        | where TimeGenerated > ago(1d) and MeterCategory_s == 'Virtual Machines'\n        | project InstanceId_s=tolower(ResourceId), UnitPrice_s, EffectivePrice_s\n        | summarize arg_max(todouble(EffectivePrice_s), *) by InstanceId_s\n        | project InstanceId_s, MonthlyCost=24*todouble(iif(todouble(UnitPrice_s) > 0, todouble(UnitPrice_s), todouble(EffectivePrice_s)))*30\n    ) on InstanceId_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n        let LastNonStopped = toscalar($vmsTableName\n        | where InstanceId_s =~ '$queryInstanceId'\n        | where TimeGenerated < now()\n        | where PowerState_s !has 'stopped'\n        | summarize max(todatetime(StatusDate_s)));\n        $consumptionTableName\n        | where ResourceId =~ '$queryInstanceId'\n        | where todatetime(Date_s) >= LastNonStopped\n        | where MeterCategory_s == 'Virtual Machines'\n        | summarize ComputeCostsSinceStopped = sum(todouble(Quantity_s)*todouble(UnitPrice_s)) by MeterSubCategory_s, StoppedSince=LastNonStopped\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.MonthlyCost \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.MonthlyCost\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"High\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"StoppedVms\"\n        RecommendationSubTypeId = \"110fea55-a9c3-480d-8248-116f61e139a8\"\n        RecommendationDescription = \"Virtual Machine is stopped (not deallocated) and still incurring costs\"\n        RecommendationAction = \"Deallocate Virtual Machine\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"stoppedvms-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\nfunction Find-SkuHourlyPrice {\n    param (\n        [object[]] $SKUPriceSheet,\n        [string] $SKUName\n    )\n\n    $skuPriceObject = $null\n\n    if ($SKUPriceSheet)\n    {\n        $skuNameParts = $SKUName.Split('_')\n\n        if ($skuNameParts.Count -eq 3) # e.g., Standard_D1_v2\n        {\n            $skuNameFilter = \"*\" + $skuNameParts[1] + \" *\"\n            $skuVersionFilter = \"*\" + $skuNameParts[2]\n            $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter `\n             -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' `\n             -and $_.MeterName_s -like $skuVersionFilter -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 }\n            \n            if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n            {\n                $skuPriceObject = $skuPrices[0]\n            }\n            if ($skuPrices.Count -gt 2) # D1-like scenarios\n            {\n                $skuFilter = \"*\" + $skuNameParts[1] + \" \" + $skuNameParts[2] + \"*\"\n                $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilter }\n    \n                if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n                {\n                    $skuPriceObject = $skuPrices[0]\n                }\n            }\n        }\n    \n        if ($skuNameParts.Count -eq 2) # e.g., Standard_D1\n        {\n            $skuNameFilter = \"*\" + $skuNameParts[1] + \"*\"\n    \n            $skuPrices = $SKUPriceSheet | Where-Object { $_.MeterName_s -like $skuNameFilter `\n             -and $_.MeterName_s -notlike '*Low Priority' -and $_.MeterName_s -notlike '*Expired' `\n             -and $_.MeterName_s -notlike '* v*' -and $_.MeterSubCategory_s -notlike '*Windows' -and $_.UnitPrice_s -ne 0 }\n            \n            if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n            {\n                $skuPriceObject = $skuPrices[0]\n            }\n            if ($skuPrices.Count -gt 2) # D1-like scenarios\n            {\n                $skuFilterLeft = \"*\" + $skuNameParts[1] + \"/*\"\n                $skuFilterRight = \"*/\" + $skuNameParts[1] + \"*\"\n                $skuPrices = $skuPrices | Where-Object { $_.MeterName_s -like $skuFilterLeft -or $_.MeterName_s -like $skuFilterRight }\n                \n                if (($skuPrices -or $skuPrices.Count -ge 1) -and $skuPrices.Count -le 2)\n                {\n                    $skuPriceObject = $skuPrices[0]\n                }\n            }\n        }    \n    }\n\n    $targetHourlyPrice = [double]::MaxValue\n    if ($null -ne $skuPriceObject)\n    {\n        $targetUnitHours = [int] (Select-String -InputObject $skuPriceObject.UnitOfMeasure_s -Pattern \"^\\d+\").Matches[0].Value\n        if ($targetUnitHours -gt 0)\n        {\n            $targetHourlyPrice = [double] ($skuPriceObject.UnitPrice_s / $targetUnitHours)\n        }\n    }\n\n    return $targetHourlyPrice\n}\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n# percentiles variables\n$cpuPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileCpu\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentile -gt 0)) {\n    $cpuPercentile = 99\n}\n$memoryPercentile = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfPercentileMemory\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentile -gt 0)) {\n    $memoryPercentile = 99\n}\n\n# perf thresholds variables\n$cpuPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuPercentageThreshold -gt 0)) {\n    $cpuPercentageThreshold = 30\n}\n$memoryPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdMemoryPercentage\" -ErrorAction SilentlyContinue)\nif (-not($memoryPercentageThreshold -gt 0)) {\n    $memoryPercentageThreshold = 50\n}\n$cpuDegradedMaxPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuDegradedMaxPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuDegradedMaxPercentageThreshold -gt 0)) {\n    $cpuDegradedMaxPercentageThreshold = 95\n}\n$cpuDegradedAvgPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdCpuDegradedAvgPercentage\" -ErrorAction SilentlyContinue)\nif (-not($cpuDegradedAvgPercentageThreshold -gt 0)) {\n    $cpuDegradedAvgPercentageThreshold = 75\n}\n$memoryDegradedPercentageThreshold = [int] (Get-AutomationVariable -Name  \"AzureOptimization_PerfThresholdMemoryDegradedPercentage\" -ErrorAction SilentlyContinue)\nif (-not($memoryDegradedPercentageThreshold -gt 0)) {\n    $memoryDegradedPercentageThreshold = 90\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$perfDaysBackwards = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfPeriodInDays\" -ErrorAction SilentlyContinue)\nif (-not($perfDaysBackwards -gt 0)) {\n    $perfDaysBackwards = 7\n}\n\n$perfTimeGrain = Get-AutomationVariable -Name  \"AzureOptimization_RecommendPerfTimeGrain\" -ErrorAction SilentlyContinue\nif (-not($perfTimeGrain)) {\n    $perfTimeGrain = \"1h\"\n}\n\n$referenceRegion = Get-AutomationVariable -Name \"AzureOptimization_ReferenceRegion\"\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVMSS','MonitorMetrics','ARGResourceContainers','AzureConsumption','Pricesheet')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$vmssTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVMSS' }).LogAnalyticsSuffix + \"_CL\"\n$metricsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'MonitorMetrics' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n$pricesheetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'Pricesheet' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $vmssTableName, $metricsTableName, $subscriptionsTableName, $pricesheetTableName and $consumptionTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\nWrite-Output \"Getting Virtual Machine SKUs for the $referenceRegion region...\"\n\n$skus = Get-AzComputeResourceSku -Location $referenceRegion | Where-Object { $_.ResourceType -eq \"virtualMachines\" }\n\nWrite-Output \"Getting the current Pricesheet...\"\n\nif ($cloudEnvironment -eq \"AzureCloud\")\n{\n    $pricesheetRegion = \"EU West\"\n}\n\ntry \n{\n    $pricesheetEntries = @()\n\n    $baseQuery = @\"\n    $pricesheetTableName\n    | where TimeGenerated > ago(14d)\n    | where MeterCategory_s == 'Virtual Machines' and MeterRegion_s == '$pricesheetRegion' and PriceType_s == 'Consumption'\n    | distinct MeterName_s, MeterSubCategory_s, MeterCategory_s, MeterRegion_s, UnitPrice_s, UnitOfMeasure_s\n\"@    \n\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days 14) -Wait 600 -IncludeStatistics\n    $pricesheetEntries = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    \n    Write-Output \"Query finished with $($pricesheetEntries.Count) results.\"   \n    Write-Output \"Query statistics: $($queryResults.Statistics.query)\"    \n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    Write-Output \"Consumption pricesheet not available, will estimate savings based in cores count...\"\n}\n\n$skuPricesFound = @{}\n\n$recommendationsErrors = 0\n\nWrite-Output \"Looking for underutilized Scale Sets, with less than $cpuPercentageThreshold% CPU and $memoryPercentageThreshold% RAM usage...\"\n\n$baseQuery = @\"\n    let billingInterval = 30d; \n    let perfInterval = $($perfDaysBackwards)d; \n    let cpuPercentileValue = $cpuPercentile;\n    let memoryPercentileValue = $memoryPercentile;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); \n    let stime = etime-billingInterval; \n\n    let BilledVMs = $consumptionTableName \n    | where todatetime(Date_s) between (stime..etime) and ResourceId contains 'virtualmachinescalesets'\n    | extend VMConsumedQuantity = iif(ResourceId contains 'virtualmachinescalesets' and MeterCategory_s == 'Virtual Machines', todouble(Quantity_s), 0.0)\n    | extend VMPrice = iif(ResourceId contains 'virtualmachinescalesets' and MeterCategory_s == 'Virtual Machines', todouble(EffectivePrice_s), 0.0)\n    | extend FinalCost = VMPrice * VMConsumedQuantity\n    | extend InstanceId_s = tolower(ResourceId)\n    | summarize Last30DaysCost = sum(FinalCost), Last30DaysQuantity = sum(VMConsumedQuantity) by InstanceId_s;\n\n    let MemoryPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where MetricNames_s == \"Available Memory Bytes\" and AggregationType_s == \"Minimum\"\n    | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024\n    | project TimeGenerated, MemoryAvailableMBs, InstanceId_s=ResourceId\n    | join kind=inner (\n        $vmssTableName \n        | where TimeGenerated > ago(1d)\n        | distinct InstanceId_s, MemoryMB_s\n    ) on InstanceId_s\n    | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 \n    | summarize PMemoryPercentage = percentile(MemoryPercentage, memoryPercentileValue) by InstanceId_s;\n\n    let ProcessorPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where MetricNames_s == \"Percentage CPU\" and AggregationType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PCPUPercentage = percentile(todouble(MetricValue_s), cpuPercentileValue) by InstanceId_s;\n\n    $vmssTableName \n    | where TimeGenerated > ago(1d)\n    | distinct InstanceId_s, VMSSName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, Tags_s\n    | join kind=inner ( BilledVMs ) on InstanceId_s \n    | join kind=leftouter ( MemoryPerf ) on InstanceId_s\n    | join kind=leftouter ( ProcessorPerf ) on InstanceId_s\n    | project InstanceId_s, VMSSName = VMSSName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, PMemoryPercentage, PCPUPercentage, Tags_s, Last30DaysCost, Last30DaysQuantity\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionId\n    | where isnotempty(PMemoryPercentage) and isnotempty(PCPUPercentage) and PMemoryPercentage < $memoryPercentageThreshold and PCPUPercentage < $cpuPercentageThreshold\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n\n    $targetSku = $null\n    $currentSku = $skus | Where-Object { $_.Name -eq $result.VMSSSize_s }\n\n    $currentSkuvCPUs = [int]($currentSku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value\n\n    $memoryNeeded = [double]($currentSku.Capabilities | Where-Object { $_.Name -eq 'MemoryGB' }).Value * ([double] $result.PMemoryPercentage / 100)\n    $cpuNeeded = [double]$currentSkuvCPUs * ([double] $result.PCPUPercentage / 100)\n    $currentPremiumIO = [bool] ($currentSku.Capabilities | Where-Object { $_.Name -eq 'PremiumIO' }).Value\n    $currentCpuArch = ($currentSku.Capabilities | Where-Object { $_.Name -eq 'CpuArchitectureType' }).Value\n\n    if ($null -eq $skuPricesFound[$currentSku.Name])\n    {\n        $skuPricesFound[$currentSku.Name] = Find-SkuHourlyPrice -SKUName $currentSku.Name -SKUPriceSheet $pricesheetEntries\n    }\n\n    $targetSkuCandidates = @()\n\n    foreach ($sku in $skus)\n    {\n        $targetSkuCandidate = $null\n\n        $skuCPUs = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'vCPUsAvailable' }).Value\n        $skuMemory = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MemoryGB' }).Value\n        $skuMaxDataDisks = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxDataDiskCount' }).Value\n        $skuMaxNICs = [int] ($sku.Capabilities | Where-Object { $_.Name -eq 'MaxNetworkInterfaces' }).Value\n        $skuPremiumIO = [bool] ($sku.Capabilities | Where-Object { $_.Name -eq 'PremiumIO' }).Value\n        $skuCpuArch = ($sku.Capabilities | Where-Object { $_.Name -eq 'CpuArchitectureType' }).Value\n\n        if ($currentSku.Name -ne $sku.Name -and -not($sku.Name -like \"*Promo*\") -and [double]$skuCPUs -ge $cpuNeeded -and [double]$skuMemory -ge $memoryNeeded `\n                -and $skuMaxDataDisks -ge [int] $result.DataDiskCount_s -and $skuMaxNICs -ge [int] $result.NicCount_s `\n                -and ($currentPremiumIO -eq $false -or $skuPremiumIO -eq $currentPremiumIO) -and $skuCpuArch -eq $currentCpuArch)\n        {\n            if ($null -eq $skuPricesFound[$sku.Name])\n            {\n                $skuPricesFound[$sku.Name] = Find-SkuHourlyPrice -SKUName $sku.Name -SKUPriceSheet $pricesheetEntries\n            }\n\n            if ($skuPricesFound[$currentSku.Name] -eq 0 -or $skuPricesFound[$sku.Name] -lt $skuPricesFound[$currentSku.Name])\n            {\n                $targetSkuCandidate = New-Object PSObject -Property @{\n                    Name = $sku.Name\n                    HourlyPrice = $skuPricesFound[$sku.Name]\n                    vCPUsAvailable = $skuCPUs\n                    MemoryGB = $skuMemory\n                }\n\n                $targetSkuCandidates += $targetSkuCandidate    \n            }\n        }\n    }\n\n    $targetSku = $targetSkuCandidates | Sort-Object -Property HourlyPrice,MemoryGB,vCPUsAvailable | Select-Object -First 1\n\n    if ($null -ne $targetSku)\n    {\n        $queryInstanceId = $result.InstanceId_s\n        $queryText = @\"\n        let billingInterval = 30d; \n        let armId = `'$queryInstanceId`';\n        let gInt = $perfTimeGrain;\n        let MemoryPerf = $metricsTableName \n        | where TimeGenerated > ago(billingInterval)\n        | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n        | where ResourceId == armId\n        | where MetricNames_s == 'Available Memory Bytes' and AggregationType_s == 'Minimum'\n        | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024\n        | project CollectedDate, MemoryAvailableMBs, InstanceId_s=ResourceId\n        | join kind=inner (\n            $vmssTableName \n            | where TimeGenerated > ago(1d)\n            | distinct InstanceId_s, MemoryMB_s\n        ) on InstanceId_s\n        | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 \n        | summarize percentile(MemoryPercentage, $memoryPercentile) by bin(CollectedDate, gInt);\n        let ProcessorPerf = $metricsTableName \n        | where TimeGenerated > ago(billingInterval) \n        | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n        | where ResourceId == armId\n        | where MetricNames_s == 'Percentage CPU' and AggregationType_s == 'Maximum'\n        | extend ProcessorPercentage = todouble(MetricValue_s)\n        | summarize percentile(ProcessorPercentage, $cpuPercentile) by bin(CollectedDate, gInt);\n        MemoryPerf\n        | join kind=inner (ProcessorPerf) on CollectedDate\n        | render timechart\n\"@\n\n        switch ($cloudEnvironment)\n        {\n            \"AzureCloud\" { $azureTld = \"com\" }\n            \"AzureChinaCloud\" { $azureTld = \"cn\" }\n            \"AzureUSGovernment\" { $azureTld = \"us\" }\n            default { $azureTld = \"com\" }\n        }\n\n        $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n        $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n        $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n        $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n    \n        $additionalInfoDictionary = @{}\n    \n        $additionalInfoDictionary[\"SupportsDataDisksCount\"] = \"true\"\n        $additionalInfoDictionary[\"SupportsNICCount\"] = \"true\"\n        $additionalInfoDictionary[\"BelowCPUThreshold\"] = \"true\"\n        $additionalInfoDictionary[\"BelowMemoryThreshold\"] = \"true\"\n        $additionalInfoDictionary[\"currentSku\"] = \"$($result.VMSSSize_s)\"\n        $additionalInfoDictionary[\"InstanceCount\"] = [int] $result.Capacity_s\n        $additionalInfoDictionary[\"targetSku\"] = \"$($targetSku.Name)\"\n        $additionalInfoDictionary[\"DataDiskCount\"] = \"$($result.DataDiskCount_s)\"\n        $additionalInfoDictionary[\"NicCount\"] = \"$($result.NicCount_s)\"\n        $additionalInfoDictionary[\"MetricCPUPercentage\"] = \"$($result.PCPUPercentage)\"\n        $additionalInfoDictionary[\"MetricMemoryPercentage\"] = \"$($result.PMemoryPercentage)\"\n    \n        $fitScore = 4 # needs disk IOPS and throughput analysis to improve score\n        \n        $fitScore = [Math]::max(0.0, $fitScore)\n\n        $savingCoefficient = [double] $currentSkuvCPUs / [double] $targetSku.vCPUsAvailable\n\n        if ($targetSku -and $null -eq $skuPricesFound[$targetSku.Name])\n        {\n            $skuPricesFound[$targetSku.Name] = Find-SkuHourlyPrice -SKUName $targetSku.Name -SKUPriceSheet $pricesheetEntries\n        }\n\n        $targetSkuSavingsMonthly = $result.Last30DaysCost - ($result.Last30DaysCost / $savingCoefficient)\n\n        $tentativeTargetSkuSavingsMonthly = -1\n\n        if ($targetSku -and $skuPricesFound[$targetSku.Name] -lt [double]::MaxValue)\n        {\n            $targetSkuPrice = $skuPricesFound[$targetSku.Name]    \n\n            if ($null -eq $skuPricesFound[$currentSku.Name])\n            {\n                $skuPricesFound[$currentSku.Name] = Find-SkuHourlyPrice -SKUName $currentSku.Name -SKUPriceSheet $pricesheetEntries\n            }\n\n            if ($skuPricesFound[$currentSku.Name] -lt [double]::MaxValue)\n            {\n                $currentSkuPrice = $skuPricesFound[$currentSku.Name]    \n                $tentativeTargetSkuSavingsMonthly = ($currentSkuPrice * [double] $result.Last30DaysQuantity) - ($targetSkuPrice * [double] $result.Last30DaysQuantity)    \n            }\n            else\n            {\n                $tentativeTargetSkuSavingsMonthly = $result.Last30DaysCost - ($targetSkuPrice * [double] $result.Last30DaysQuantity)    \n            }\n        }\n\n        if ($tentativeTargetSkuSavingsMonthly -ge 0)\n        {\n            $targetSkuSavingsMonthly = $tentativeTargetSkuSavingsMonthly\n        }\n\n        $tags = @{}\n\n        if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n        {\n            $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n            foreach ($tagPairString in $tagPairs)\n            {\n                $tagPair = $tagPairString.Split('=')\n                if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n                {\n                    $tagName = $tagPair[0].Trim()\n                    $tagValue = $tagPair[1].Trim()\n                    $tags[$tagName] = $tagValue    \n                }\n            }\n        }            \n    \n        if ($targetSkuSavingsMonthly -eq [double]::PositiveInfinity)\n        {\n            $targetSkuSavingsMonthly = [double] $result.Last30DaysCost / 2\n        }\n\n        $additionalInfoDictionary[\"savingsAmount\"] = [double] $targetSkuSavingsMonthly     \n        $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    \n        $recommendation = New-Object PSObject -Property @{\n            Timestamp                   = $timestamp\n            Cloud                       = $result.Cloud_s\n            Category                    = \"Cost\"\n            ImpactedArea                = \"Microsoft.Compute/virtualMachineScaleSets\"\n            Impact                      = \"High\"\n            RecommendationType          = \"Saving\"\n            RecommendationSubType       = \"UnderusedVMSS\"\n            RecommendationSubTypeId     = \"a4955cc9-533d-46a2-8625-5c4ebd1c30d5\"\n            RecommendationDescription   = \"VM Scale Set has been underutilized\"\n            RecommendationAction        = \"Resize VM Scale Set to lower SKU or scale it in\"\n            InstanceId                  = $result.InstanceId_s\n            InstanceName                = $result.VMSSName\n            AdditionalInfo              = $additionalInfoDictionary\n            ResourceGroup               = $result.ResourceGroup\n            SubscriptionGuid            = $result.SubscriptionId\n            SubscriptionName            = $result.SubscriptionName\n            TenantGuid                  = $result.TenantGuid_g\n            FitScore                    = $fitScore\n            Tags                        = $tags\n            DetailsURL                  = $detailsURL\n        }\n    \n        $recommendations += $recommendation        \n    }\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"vmss-underutilized-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for performance constrained Scale Sets, with more than $cpuDegradedMaxPercentageThreshold% Max. CPU, $cpuDegradedAvgPercentageThreshold% Avg. CPU and $memoryDegradedPercentageThreshold% RAM usage...\"\n\n$baseQuery = @\"\n    let perfInterval = $($perfDaysBackwards)d; \n\n    let MemoryPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where MetricNames_s == \"Available Memory Bytes\" and AggregationType_s == \"Minimum\"\n    | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024\n    | project TimeGenerated, MemoryAvailableMBs, InstanceId_s=ResourceId\n    | join kind=inner (\n        $vmssTableName \n        | where TimeGenerated > ago(1d)\n        | distinct InstanceId_s, MemoryMB_s\n    ) on InstanceId_s\n    | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 \n    | summarize PMemoryPercentage = avg(MemoryPercentage) by InstanceId_s;\n\n    let ProcessorMaxPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where MetricNames_s == \"Percentage CPU\" and AggregationType_s == 'Maximum'\n    | extend InstanceId_s = ResourceId\n    | summarize PCPUMaxPercentage = avg(todouble(MetricValue_s)) by InstanceId_s;\n\n    let ProcessorAvgPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | where MetricNames_s == \"Percentage CPU\" and AggregationType_s == 'Average'\n    | extend InstanceId_s = ResourceId\n    | summarize PCPUAvgPercentage = avg(todouble(MetricValue_s)) by InstanceId_s;\n\n    $vmssTableName \n    | where TimeGenerated > ago(1d)\n    | distinct InstanceId_s, VMSSName_s, ResourceGroupName_s, SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, Tags_s\n    | join kind=leftouter ( MemoryPerf ) on InstanceId_s\n    | join kind=leftouter ( ProcessorMaxPerf ) on InstanceId_s\n    | join kind=leftouter ( ProcessorAvgPerf ) on InstanceId_s\n    | project InstanceId_s, VMSSName = VMSSName_s, ResourceGroup = ResourceGroupName_s, SubscriptionId = SubscriptionGuid_g, Cloud_s, TenantGuid_g, VMSSSize_s, NicCount_s, DataDiskCount_s, Capacity_s, PMemoryPercentage, PCPUMaxPercentage, PCPUAvgPercentage, Tags_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionId\n    | where isnotempty(PMemoryPercentage) and isnotempty(PCPUAvgPercentage) and isnotempty(PCPUMaxPercentage) and (PMemoryPercentage > $memoryDegradedPercentageThreshold or (PCPUMaxPercentage > $cpuDegradedMaxPercentageThreshold and PCPUAvgPercentage > $cpuDegradedAvgPercentageThreshold))\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n    let perfInterval = $($perfDaysBackwards)d; \n    let armId = `'$queryInstanceId`';\n    let gInt = $perfTimeGrain;\n    let MemoryPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval)\n    | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n    | where ResourceId == armId\n    | where MetricNames_s == 'Available Memory Bytes' and AggregationType_s == 'Minimum'\n    | extend MemoryAvailableMBs = todouble(MetricValue_s)/1024/1024\n    | project CollectedDate, MemoryAvailableMBs, InstanceId_s=ResourceId\n    | join kind=inner (\n        $vmssTableName \n        | where TimeGenerated > ago(1d)\n        | distinct InstanceId_s, MemoryMB_s\n    ) on InstanceId_s\n    | extend MemoryPercentage = todouble(toint(MemoryMB_s) - toint(MemoryAvailableMBs)) / todouble(MemoryMB_s) * 100 \n    | summarize avg(MemoryPercentage) by bin(CollectedDate, gInt);\n    let ProcessorMaxPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n    | where ResourceId == armId\n    | where MetricNames_s == 'Percentage CPU' and AggregationType_s == 'Maximum'\n    | extend ProcessorMaxPercentage = todouble(MetricValue_s)\n    | summarize percentile(ProcessorMaxPercentage, $cpuPercentile) by bin(CollectedDate, gInt);\n    let ProcessorAvgPerf = $metricsTableName \n    | where TimeGenerated > ago(perfInterval) \n    | extend CollectedDate = todatetime(strcat(format_datetime(TimeGenerated, 'yyyy-MM-dd'),'T',format_datetime(TimeGenerated, 'HH'),':00:00Z'))\n    | where ResourceId == armId\n    | where MetricNames_s == 'Percentage CPU' and AggregationType_s == 'Average'\n    | extend ProcessorAvgPercentage = todouble(MetricValue_s)\n    | summarize percentile(ProcessorAvgPercentage, $cpuPercentile) by bin(CollectedDate, gInt);\n    MemoryPerf\n    | join kind=inner (ProcessorMaxPerf) on CollectedDate\n    | join kind=inner (ProcessorAvgPerf) on CollectedDate\n    | render timechart\n\"@\n\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $datetime.AddDays(-30).ToString(\"yyyy-MM-dd\")\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = \"$($result.VMSSSize_s)\"\n    $additionalInfoDictionary[\"InstanceCount\"] = [int] $result.Capacity_s\n    $additionalInfoDictionary[\"MetricCPUAvgPercentage\"] = \"$($result.PCPUAvgPercentage)\"\n    $additionalInfoDictionary[\"MetricCPUMaxPercentage\"] = \"$($result.PCPUMaxPercentage)\"\n    $additionalInfoDictionary[\"MetricMemoryPercentage\"] = \"$($result.PMemoryPercentage)\"\n\n    $fitScore = 3 # needs disk IOPS and throughput analysis to improve score\n\n    if ([double] $result.PCPUMaxPercentage -gt [double] $cpuDegradedMaxPercentageThreshold -and [double] $result.PCPUAvgPercentage -gt [double] $cpuDegradedAvgPercentageThreshold)\n    {\n        $fitScore = 4\n    }\n    \n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }            \n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp                   = $timestamp\n        Cloud                       = $result.Cloud_s\n        Category                    = \"Performance\"\n        ImpactedArea                = \"Microsoft.Compute/virtualMachineScaleSets\"\n        Impact                      = \"Medium\"\n        RecommendationType          = \"BestPractices\"\n        RecommendationSubType       = \"PerfConstrainedVMSS\"\n        RecommendationSubTypeId     = \"20a40c62-e5c8-4cc3-9fc2-f4ac75013182\"\n        RecommendationDescription   = \"VM Scale Set performance has been constrained by lack of resources\"\n        RecommendationAction        = \"Resize VM Scale Set to higher SKU or scale it out\"\n        InstanceId                  = $result.InstanceId_s\n        InstanceName                = $result.VMSSName\n        AdditionalInfo              = $additionalInfoDictionary\n        ResourceGroup               = $result.ResourceGroup\n        SubscriptionGuid            = $result.SubscriptionId\n        SubscriptionName            = $result.SubscriptionName\n        TenantGuid                  = $result.TenantGuid_g\n        FitScore                    = $fitScore\n        Tags                        = $tags\n        DetailsURL                  = $detailsURL\n    }\n\n    $recommendations += $recommendation        \n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"vmss-perfconstrained-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGVirtualMachine','ARGUnmanagedDisk','ARGAvailabilitySet','ARGResourceContainers','ARGVMSS')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$availSetTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGAvailabilitySet' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n$vmsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualMachine' }).LogAnalyticsSuffix + \"_CL\"\n$vhdsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGUnmanagedDisk' }).LogAnalyticsSuffix + \"_CL\"\n$vmssTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVMSS' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $availSetTableName, $vmsTableName, $vmssTableName, $vhdsTableName and $subscriptionsTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 1\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\nWrite-Output \"Looking for Availability Sets with a low fault domain count...\"\n\n# Execute the recommendation query against Log Analytics\n\n$baseQuery = @\"\n    $availSetTableName\n    | where TimeGenerated > ago(1d) and toint(FaultDomains_s) < 3 and toint(FaultDomains_s) < todouble(VmCount_s)/2\n    | project TimeGenerated, InstanceId_s, InstanceName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s, FaultDomains_s, VmCount_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"FaultDomainCount\"] = $result.FaultDomains_s\n    $additionalInfoDictionary[\"VMCount\"] = $result.VmCount_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"AvailSetLowFaultDomainCount\"\n        RecommendationSubTypeId = \"255de20b-d5e4-4be5-9695-620b4a905774\"\n        RecommendationDescription = \"Availability Sets should have a fault domain count of 3 or equal or greater than half of the Virtual Machines count\"\n        RecommendationAction = \"Increase the fault domain count of your Availability Set\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.InstanceName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"availsetsfaultdomaincount-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for Availability Sets with a low update domain count...\"\n\n$baseQuery = @\"\n    $availSetTableName\n    | where TimeGenerated > ago(1d) and toint(UpdateDomains_s) < todouble(VmCount_s)/2\n    | project TimeGenerated, InstanceId_s, InstanceName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s, Tags_s, UpdateDomains_s, VmCount_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g        \n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"UpdateDomainCount\"] = $result.UpdateDomains_s\n    $additionalInfoDictionary[\"VMCount\"] = $result.VmCount_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"AvailSetLowUpdateDomainCount\"\n        RecommendationSubTypeId = \"9764e285-2eca-46c5-b49e-649c039cf0cf\"\n        RecommendationDescription = \"Availability Sets should have an update domain count equal or greater than half of the Virtual Machines count\"\n        RecommendationAction = \"Increase the update domain count of your Availability Set\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.InstanceName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"availsetsupdatedomaincount-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for Availability Sets with VMs sharing storage accounts...\"\n\n$baseQuery = @\"\n    $vhdsTableName\n    | where TimeGenerated > ago(1d)\n    | extend StorageAccountName = tostring(split(InstanceId_s, '/')[0])\n    | distinct TimeGenerated, StorageAccountName, OwnerVMId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s\n    | join kind=inner (\n        $vmsTableName\n        | where TimeGenerated > ago(1d) and isnotempty(AvailabilitySetId_s)\n        | distinct VMName_s, InstanceId_s, AvailabilitySetId_s, Cloud_s, Tags_s\n    ) on `$left.OwnerVMId_s == `$right.InstanceId_s\n    | extend AvailabilitySetName = tostring(split(AvailabilitySetId_s,'/')[8])\n    | summarize TimeGenerated = any(TimeGenerated), Tags_s=any(Tags_s), VMCount = count() by AvailabilitySetName, AvailabilitySetId_s, StorageAccountName, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s\n    | where VMCount > 1\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics -ErrorAction Continue\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.AvailabilitySetId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"SharedStorageAccountName\"] = $result.StorageAccountName\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"AvailSetSharedStorageAccount\"\n        RecommendationSubTypeId = \"e530029f-9b6a-413a-99ed-81af54502bb9\"\n        RecommendationDescription = \"Virtual Machines in unmanaged Availability Sets should not share the same Storage Account\"\n        RecommendationAction = \"Migrate Virtual Machines disks to Managed Disks or keep the disks in a dedicated Storage Account per VM\"\n        InstanceId = $result.AvailabilitySetId_s\n        InstanceName = $result.AvailabilitySetName\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"availsetsharedsa-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for Storage Accounts with multiple VMs...\"\n\n$baseQuery = @\"\n    $vhdsTableName\n    | where TimeGenerated > ago(1d)\n    | extend StorageAccountName = tostring(split(InstanceId_s, '/')[0])\n    | distinct TimeGenerated, StorageAccountName, OwnerVMId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, Cloud_s\n    | join kind=inner ( \n        $vmsTableName\n        | where TimeGenerated > ago(1d)\n        | distinct InstanceId_s, Tags_s\n    ) on `$left.OwnerVMId_s == `$right.InstanceId_s\n    | summarize TimeGenerated = any(TimeGenerated), Tags_s=any(Tags_s), VMCount = count() by StorageAccountName, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, Cloud_s\n    | where VMCount > 1\n    | extend StorageAccountId = strcat('/subscriptions/', SubscriptionGuid_g, '/resourcegroups/', ResourceGroupName_s, '/providers/microsoft.storage/storageaccounts/', StorageAccountName)\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics -ErrorAction Continue\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.StorageAccountId\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"VirtualMachineCount\"] = $result.VMCount\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"StorageAccountsMultipleVMs\"\n        RecommendationSubTypeId = \"b70f44fa-5ef9-4180-b2f9-9cc6be07ab3e\"\n        RecommendationDescription = \"Virtual Machines with unmanaged disks should not share the same Storage Account\"\n        RecommendationAction = \"Migrate Virtual Machines disks to Managed Disks or keep the disks in a dedicated Storage Account per VM\"\n        InstanceId = $result.StorageAccountId\n        InstanceName = $result.StorageAccountName\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"storageaccountsmultiplevms-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMs with no Availability Set...\"\n\n$baseQuery = @\"\n    $vmsTableName\n    | where TimeGenerated > ago(1d) and isempty(AvailabilitySetId_s) and isempty(Zones_s) and Tags_s !has 'databricks-instance-name'\n    | project TimeGenerated, VMName_s, InstanceId_s, Tags_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"VMsNoAvailSet\"\n        RecommendationSubTypeId = \"998b50d8-e654-417b-ab20-a31cb11629c0\"\n        RecommendationDescription = \"Virtual Machines should be placed in an Availability Set together with other instances with the same role\"\n        RecommendationAction = \"Add VM to an Availability Set together with other VMs of the same role\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"vmsnoavailset-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMs alone in an Availability Set...\"\n\n$baseQuery = @\"\n    $vmsTableName\n    | where TimeGenerated > ago(1d) and isnotempty(AvailabilitySetId_s) and isempty(Zones_s)\n    | distinct TimeGenerated, VMName_s, InstanceId_s, AvailabilitySetId_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s, Tags_s\n    | summarize any(TimeGenerated, VMName_s, InstanceId_s, Tags_s), VMCount = count() by AvailabilitySetId_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s\n    | where VMCount == 1\n    | project TimeGenerated = any_TimeGenerated, VMName_s = any_VMName_s, InstanceId_s = any_InstanceId_s, Tags_s = any_Tags_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Cloud_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g        \n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"VMsSingleInAvailSet\"\n        RecommendationSubTypeId = \"fe577af5-dfa2-413a-82a9-f183196c1f49\"\n        RecommendationDescription = \"Virtual Machines should not be the only instance in an Availability Set\"\n        RecommendationAction = \"Add more VMs of the same role to the Availability Set\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"vmssingleinavailset-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMs with disks in multiple Storage Accounts...\"\n\n$baseQuery = @\"\n    $vhdsTableName\n    | where TimeGenerated > ago(1d)\n    | extend StorageAccountName = tostring(split(InstanceId_s, '/')[0])\n    | distinct TimeGenerated, StorageAccountName, OwnerVMId_s\n    | summarize TimeGenerated = any(TimeGenerated), StorageAcccountCount = count() by OwnerVMId_s\n    | where StorageAcccountCount > 1\n    | join kind=inner (\n        $vmsTableName\n        | where TimeGenerated > ago(1d)\n        | distinct VMName_s, InstanceId_s, Cloud_s, TenantGuid_g, SubscriptionGuid_g, ResourceGroupName_s, Tags_s\n    ) on `$left.OwnerVMId_s == `$right.InstanceId_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics -ErrorAction Continue\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"StorageAccountsUsed\"] = $result.StorageAcccountCount\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"DisksMultipleStorageAccounts\"\n        RecommendationSubTypeId = \"024049e7-f63a-4e1c-b620-f011aafbc576\"\n        RecommendationDescription = \"Each Virtual Machine should have its unmanaged disks stored in a single Storage Account for higher availability and manageability\"\n        RecommendationAction = \"Migrate Virtual Machines disks to Managed Disks or move VHDs to the same Storage Account\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"disksmultiplesa-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMs using unmanaged disks...\"\n\n$baseQuery = @\"\n    $vmsTableName \n    | where TimeGenerated > ago(1d) and UsesManagedDisks_s == 'false'\n    | distinct InstanceId_s, VMName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, DeploymentModel_s, Tags_s, Cloud_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g        \n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"DeploymentModel\"] = $result.DeploymentModel_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"UnmanagedDisks\"\n        RecommendationSubTypeId = \"b576a069-b1f2-43a6-9134-5ee75376402a\"\n        RecommendationDescription = \"Virtual Machines should use Managed Disks for higher availability and manageability\"\n        RecommendationAction = \"Migrate Virtual Machines disks to Managed Disks\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"unmanageddisks-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for Resource Groups with VMs not in multiple AZs...\"\n\n$baseQuery = @\"\n    let VMsInZones = materialize($vmsTableName\n    | where TimeGenerated > ago(1d) and isempty(AvailabilitySetId_s) and isnotempty(Zones_s));\n    VMsInZones\n    | distinct ResourceGroupName_s, Zones_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s\n    | summarize ZonesCount=count() by ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Cloud_s\n    | where ZonesCount < 3\n    | join kind=inner ( \n        VMsInZones\n        | where PowerState_s has 'running'\n        | distinct VMName_s, ResourceGroupName_s, SubscriptionGuid_g\n        | summarize VMCount=count() by ResourceGroupName_s, SubscriptionGuid_g\n    ) on ResourceGroupName_s and SubscriptionGuid_g\n    | where VMCount == 1 or VMCount > ZonesCount\n    | project-away SubscriptionGuid_g1, ResourceGroupName_s1\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n    | extend InstanceId = strcat('/subscriptions/', SubscriptionGuid_g, '/resourcegroups/', ResourceGroupName_s)        \n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"ZonesCount\"] = $result.ZonesCount\n    $additionalInfoDictionary[\"VMsCount\"] = $result.VMCount\n\n    $fitScore = 4 # a resource group may contain VMs from multiple applications which may lead to false negatives\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachines\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"VMsMultipleAZs\"\n        RecommendationSubTypeId = \"1a77887c-7375-434e-af19-c2543171e0b8\"\n        RecommendationDescription = \"Virtual Machines should be placed in multiple Availability Zones\"\n        RecommendationAction = \"Distribute Virtual Machines instances of the same role in multiple Availability Zones\"\n        InstanceId = $result.InstanceId\n        InstanceName = $result.ResourceGroupName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"vmsmultipleazs-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMSS not in multiple AZs...\"\n\n$baseQuery = @\"\n    $vmssTableName\n    | where TimeGenerated > ago(1d) \n    | where (isempty(Zones_s) and toint(Capacity_s) > 1) or (array_length(split(Zones_s, ' ')) != 3 and toint(Capacity_s) > 2)\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"Zones\"] = $result.Zones_s\n    $additionalInfoDictionary[\"VMSSCapacity\"] = $result.Capacity_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachineScaleSets\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"VMSSMultipleAZs\"\n        RecommendationSubTypeId = \"47e5457c-b345-4372-b536-8887fa8f0298\"\n        RecommendationDescription = \"Virtual Machine Scale Sets should be placed in multiple Availability Zones\"\n        RecommendationAction = \"Reprovision the Scale Set leveraging enough Availability Zones\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMSSName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"vmssmultipleazs-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for VMSS using unmanaged disks...\"\n\n$baseQuery = @\"\n    $vmssTableName\n    | where TimeGenerated > ago(1d) and UsesManagedDisks_s == 'false'\n    | distinct InstanceId_s, VMSSName_s, ResourceGroupName_s, SubscriptionGuid_g, TenantGuid_g, Tags_s, Cloud_s\n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g        \n\"@\n\ntry \n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)\n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"HighAvailability\"\n        ImpactedArea = \"Microsoft.Compute/virtualMachineScaleSets\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"UnmanagedDisksVMSS\"\n        RecommendationSubTypeId = \"1bf03c4a-c402-4e6c-bf20-051b18af30e2\"\n        RecommendationDescription = \"Virtual Machine Scale Sets should use Managed Disks for higher availability and manageability\"\n        RecommendationAction = \"Migrate Virtual Machine Scale Sets disks to Managed Disks\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.VMSSName_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"unmanageddisksvmss-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1",
    "content": "$ErrorActionPreference = \"Stop\"\n\n# Collect generic and recommendation-specific variables\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$workspaceId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceId\"\n$workspaceName = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceName\"\n$workspaceRG = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceRG\"\n$workspaceSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceSubId\"\n$workspaceTenantId = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsWorkspaceTenantId\"\n\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"recommendationsexports\"\n}\n\n$deploymentDate = Get-AutomationVariable -Name  \"AzureOptimization_DeploymentDate\" # yyyy-MM-dd format\n$deploymentDate = $deploymentDate.Replace('\"', \"\")\n\n$lognamePrefix = Get-AutomationVariable -Name  \"AzureOptimization_LogAnalyticsLogPrefix\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($lognamePrefix))\n{\n    $lognamePrefix = \"AzureOptimization\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n\n$subnetMaxUsedThresholdVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($subnetMaxUsedThresholdVar) -or $subnetMaxUsedThresholdVar -eq 0)\n{\n    $subnetMaxUsedThreshold = 80\n}\nelse\n{\n    $subnetMaxUsedThreshold = [int] $subnetMaxUsedThresholdVar\n}\n\n$subnetMinUsedThresholdVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($subnetMinUsedThresholdVar) -or $subnetMinUsedThresholdVar -eq 0)\n{\n    $subnetMinUsedThreshold = 5\n}\nelse\n{\n    $subnetMinUsedThreshold = [int] $subnetMinUsedThresholdVar\n}\n\n# must be a comma-separated, single-quote enclosed list of subnet names, e.g., 'gatewaysubnet','azurebastionsubnet'\n$subnetFreeExclusions = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationVNetSubnetUsedPercentageExclusions\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($subnetFreeExclusions))\n{\n    $subnetFreeExclusions = \"'gatewaysubnet'\"\n}\n\n$subnetMinAgeVar = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($subnetMinAgeVar) -or $subnetMinAgeVar -eq 0)\n{\n    $subnetMinAge = 30\n}\nelse\n{\n    $subnetMinAge = [int] $subnetMinAgeVar\n}\n\n$consumptionOffsetDays = [int] (Get-AutomationVariable -Name  \"AzureOptimization_ConsumptionOffsetDays\")\n$consumptionOffsetDaysStart = $consumptionOffsetDays + 1\n\n$SqlTimeout = 120\n$LogAnalyticsIngestControlTable = \"LogAnalyticsIngestControl\"\n\n# Authenticate against Azure\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\nWrite-Output \"Finding tables where recommendations will be generated from...\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = \"SELECT * FROM [dbo].[$LogAnalyticsIngestControlTable] WHERE CollectedType IN ('ARGNetworkInterface','ARGVirtualNetwork','ARGResourceContainers', 'ARGNSGRule', 'ARGPublicIP','AzureConsumption')\"\n    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $controlRows = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($controlRows) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\n$nicsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGNetworkInterface' }).LogAnalyticsSuffix + \"_CL\"\n$vNetsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGVirtualNetwork' }).LogAnalyticsSuffix + \"_CL\"\n$subscriptionsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGResourceContainers' }).LogAnalyticsSuffix + \"_CL\"\n$nsgRulesTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGNSGRule' }).LogAnalyticsSuffix + \"_CL\"\n$publicIpsTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'ARGPublicIP' }).LogAnalyticsSuffix + \"_CL\"\n$consumptionTableName = $lognamePrefix + ($controlRows | Where-Object { $_.CollectedType -eq 'AzureConsumption' }).LogAnalyticsSuffix + \"_CL\"\n\nWrite-Output \"Will run query against tables $nicsTableName, $nsgRulesTableName, $publicIpsTableName, $subscriptionsTableName, $consumptionTableName and $vNetsTableName\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$recommendationSearchTimeSpan = 30 + $consumptionOffsetDaysStart\n\n# Grab a context reference to the Storage Account where the recommendations file will be stored\n\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$sa = Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink\n\nif ($workspaceSubscriptionId -ne $storageAccountSinkSubscriptionId)\n{\n    Select-AzSubscription -SubscriptionId $workspaceSubscriptionId\n}\n\n$recommendationsErrors = 0\n\nWrite-Output \"Looking for subnets with free IP space less than $subnetMaxUsedThreshold%, excluding $subnetFreeExclusions...\"\n\n$baseQuery = @\"\n    $vNetsTableName\n    | where TimeGenerated > ago(1d)\n    | where SubnetName_s !in ($subnetFreeExclusions)\n    | extend FreeIPs = toint(SubnetTotalPrefixIPs_s) - toint(SubnetUsedIPs_s)\n    | extend UsedIPPercentage = (todouble(SubnetUsedIPs_s) / todouble(SubnetTotalPrefixIPs_s)) * 100\n    | where UsedIPPercentage >= $subnetMaxUsedThreshold\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/subnets\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"subnetName\"] = $result.SubnetName_s\n    $additionalInfoDictionary[\"subnetPrefix\"] = $result.SubnetPrefix_s \n    $additionalInfoDictionary[\"subnetTotalIPs\"] = $result.SubnetTotalPrefixIPs_s \n    $additionalInfoDictionary[\"subnetFreeIPs\"] = $result.FreeIPs \n    $additionalInfoDictionary[\"subnetUsedIPPercentage\"] = $result.UsedIPPercentage \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Network/virtualNetworks\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"HighSubnetIPSpaceUsage\"\n        RecommendationSubTypeId = \"5292525b-5095-4e52-803e-e17192f1d099\"\n        RecommendationDescription = \"Subnets with a high IP space usage may constrain operations\"\n        RecommendationAction = \"Move network devices to a subnet with a larger address space\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = \"$($result.VNetName_s)/$($result.SubnetName_s)\"\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"subnetshighspaceusage-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for subnets with used IP space less than $subnetMinUsedThreshold%...\"\n\n$baseQuery = @\"\n    $vNetsTableName\n    | where TimeGenerated > ago(1d)\n    | where SubnetName_s !in ($subnetFreeExclusions)\n    | extend FreeIPs = toint(SubnetTotalPrefixIPs_s) - toint(SubnetUsedIPs_s)\n    | extend UsedIPPercentage = (todouble(SubnetUsedIPs_s) / todouble(SubnetTotalPrefixIPs_s)) * 100\n    | where UsedIPPercentage > 0 and UsedIPPercentage <= $subnetMinUsedThreshold\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/subnets\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"subnetName\"] = $result.SubnetName_s\n    $additionalInfoDictionary[\"subnetPrefix\"] = $result.SubnetPrefix_s \n    $additionalInfoDictionary[\"subnetTotalIPs\"] = $result.SubnetTotalPrefixIPs_s \n    $additionalInfoDictionary[\"subnetUsedIPs_s\"] = $result.SubnetUsedIPs_s\n    $additionalInfoDictionary[\"subnetUsedIPPercentage\"] = $result.UsedIPPercentage \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Network/virtualNetworks\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"LowSubnetIPSpaceUsage\"\n        RecommendationSubTypeId = \"0f27b41c-869a-4563-86e9-d1c94232ba81\"\n        RecommendationDescription = \"Subnets with a low IP space usage are a waste of virtual network address space\"\n        RecommendationAction = \"Move network devices to a subnet with a smaller address space\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = \"$($result.VNetName_s)/$($result.SubnetName_s)\"\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"subnetslowspaceusage-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for subnets without any device...\"\n\n$baseQuery = @\"\n    $vNetsTableName\n    | where TimeGenerated > ago(1d)\n    | where toint(SubnetUsedIPs_s) == 0 and toint(SubnetDelegationsCount_s) == 0\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/subnets\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"subnetName\"] = $result.SubnetName_s\n    $additionalInfoDictionary[\"subnetPrefix\"] = $result.SubnetPrefix_s \n    $additionalInfoDictionary[\"subnetTotalIPs\"] = $result.SubnetTotalPrefixIPs_s \n    $additionalInfoDictionary[\"subnetUsedIPs_s\"] = $result.SubnetUsedIPs_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Network/virtualNetworks\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"NoSubnetIPSpaceUsage\"\n        RecommendationSubTypeId = \"343bbfb7-5bec-4711-8353-398454d42b7b\"\n        RecommendationDescription = \"Subnets without any IP usage are a waste of virtual network address space\"\n        RecommendationAction = \"Delete the subnet to reclaim address space\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = \"$($result.VNetName_s)/$($result.SubnetName_s)\"\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"subnetsnospaceusage-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for orphaned NICs...\"\n\n$baseQuery = @\"\n    $nicsTableName\n    | where TimeGenerated > ago(1d)\n    | where isempty(OwnerVMId_s) and isempty(OwnerPEId_s)\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.InstanceId_s\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"privateIpAddress\"] = $result.PrivateIPAddress_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"OperationalExcellence\"\n        ImpactedArea = \"Microsoft.Network/networkInterfaces\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"OrphanedNIC\"\n        RecommendationSubTypeId = \"4c5c2d0c-b6a4-4c59-bc18-6fff6c1f5b23\"\n        RecommendationDescription = \"Orphaned Network Interfaces (without owner VM or PE) unnecessarily consume IP address space\"\n        RecommendationAction = \"Delete the NIC to reclaim address space\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.Name_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"orphanednics-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for NSG rules referring empty or removed subnets...\"\n\n$baseQuery = @\"\n    let MinimumSubnetAge = $($subnetMinAge)d;\n    let SubnetsToday = materialize( $vNetsTableName\n    | where TimeGenerated > ago(1d)\n    | extend SubnetId = tolower(strcat(InstanceId_s, '/subnets/', SubnetName_s))\n    | distinct SubnetId, SubnetPrefix_s, SubnetUsedIPs_s, SubnetDelegationsCount_s );\n    let SubnetsBefore = materialize( $vNetsTableName\n    | where TimeGenerated < ago(1d)\n    | extend SubnetId = tolower(strcat(InstanceId_s, '/subnets/', SubnetName_s))\n    | summarize ExistsSince = min(todatetime(StatusDate_s)) by SubnetId, SubnetPrefix_s );\n    let SubnetsExistingLongEnoughIds = SubnetsBefore | where ExistsSince < ago(MinimumSubnetAge) | distinct SubnetId;\n    let EmptySubnets = SubnetsToday | where SubnetId in (SubnetsExistingLongEnoughIds) and toint(SubnetUsedIPs_s) == 0 and toint(SubnetDelegationsCount_s) == 0;\n    let SubnetsTodayIds = SubnetsToday | distinct SubnetId;\n    let SubnetsTodayPrefixes = SubnetsToday | distinct SubnetPrefix_s;\n    let RemovedSubnets = SubnetsBefore | where SubnetId !in (SubnetsTodayIds) and SubnetPrefix_s !in (SubnetsTodayPrefixes);\n    let NSGRules = materialize($nsgRulesTableName\n    | where TimeGenerated > ago(1d)\n    | extend SourceAddresses = split(RuleSourceAddresses_s,',')\n    | mvexpand SourceAddresses\n    | extend SourceAddress = tostring(SourceAddresses)\n    | extend DestinationAddresses = split(RuleDestinationAddresses_s,',')\n    | mvexpand DestinationAddresses\n    | extend DestinationAddress = tostring(DestinationAddresses)\n    | project NSGId = InstanceId_s, RuleName_s, DestinationAddress, SourceAddress, SubscriptionGuid_g, Cloud_s, TenantGuid_g, ResourceGroupName_s, NSGName = NSGName_s, Tags_s);\n    let EmptySubnetsAsSource = EmptySubnets\n    | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.SourceAddress\n    | extend SubnetState = 'empty';\n    let EmptySubnetsAsDestination = EmptySubnets\n    | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.DestinationAddress\n    | extend SubnetState = 'empty';\n    let RemovedSubnetsAsSource = RemovedSubnets\n    | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.SourceAddress\n    | extend SubnetState = 'inexisting';\n    let RemovedSubnetsAsDestination = RemovedSubnets\n    | join kind=inner ( NSGRules ) on `$left.SubnetPrefix_s == `$right.DestinationAddress\n    | extend SubnetState = 'inexisting';\n    EmptySubnetsAsSource\n    | union EmptySubnetsAsDestination\n    | union RemovedSubnetsAsSource\n    | union RemovedSubnetsAsDestination\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n    | where isnotempty(SubnetPrefix_s)\n    | distinct NSGId, NSGName, RuleName_s, SubscriptionGuid_g, SubscriptionName, ResourceGroupName_s, TenantGuid_g, Cloud_s, SubnetId, SubnetPrefix_s, SubnetState, Tags_s\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.NSGId\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"subnetId\"] = $result.SubnetId\n    $additionalInfoDictionary[\"subnetPrefix\"] = $result.SubnetPrefix_s\n    $additionalInfoDictionary[\"subnetState\"] = $result.SubnetState\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Security\"\n        ImpactedArea = \"Microsoft.Network/networkSecurityGroups\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"NSGRuleForEmptyOrInexistingSubnet\"\n        RecommendationSubTypeId = \"b5491cde-f76c-4423-8c4c-89e3558ff2f2\"\n        RecommendationDescription = \"NSG rules referring to empty or inexisting subnets\"\n        RecommendationAction = \"Update or remove the NSG rule to improve your network security posture\"\n        InstanceId = $result.NSGId\n        InstanceName = \"$($result.NSGName)/$($result.RuleName_s)\"\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"nsgrules-emptyinexistingsubnets-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for NSG rules referring orphan or removed NICs...\"\n\n$baseQuery = @\"\n    let NICsToday = materialize( $nicsTableName\n    | where TimeGenerated > ago(1d)\n    | extend NICId = tolower(InstanceId_s)\n    | distinct NICId, PrivateIPAddress_s, PublicIPId_s, OwnerVMId_s, OwnerPEId_s );\n    let NICsBefore = $nicsTableName\n    | where TimeGenerated < ago(1d)\n    | extend NICId = tolower(InstanceId_s)\n    | distinct NICId, PrivateIPAddress_s, PublicIPId_s;\n    let OrphanNICs = NICsToday \n    | where isempty(OwnerVMId_s) and isempty(OwnerPEId_s)\n    | extend PublicIPId_s = tolower(PublicIPId_s)\n    | join kind=leftouter ( \n        $publicIpsTableName\n        | where TimeGenerated > ago(1d)\n        | project PublicIPId_s = tolower(InstanceId_s), PublicIPAddress = IPAddress \n    ) on PublicIPId_s;\n    let NICsTodayIds = NICsToday | distinct NICId;\n    let NICsTodayIPs = NICsToday | distinct PrivateIPAddress_s;\n    let RemovedNICs = NICsBefore \n    | where NICId  !in (NICsTodayIds) and PrivateIPAddress_s  !in (NICsTodayIPs)\n    | extend PublicIPId_s = tolower(PublicIPId_s)\n    | join kind=leftouter ( \n        $publicIpsTableName\n        | where TimeGenerated < ago(1d)\n        | project PublicIPId_s = tolower(InstanceId_s), PublicIPAddress = IPAddress \n    ) on PublicIPId_s;\n    let NSGRules = materialize($nsgRulesTableName\n    | where TimeGenerated > ago(1d)\n    | extend SourceAddresses = split(RuleSourceAddresses_s,',')\n    | mvexpand SourceAddresses\n    | extend SourceAddress = replace('/32','',tostring(SourceAddresses))\n    | extend DestinationAddresses = split(RuleDestinationAddresses_s,',')\n    | mvexpand DestinationAddresses\n    | extend DestinationAddress = replace('/32','',tostring(DestinationAddresses))\n    | project NSGId = InstanceId_s, RuleName_s, DestinationAddress, SourceAddress, SubscriptionGuid_g, Cloud_s, TenantGuid_g, ResourceGroupName_s, NSGName = NSGName_s, Tags_s);\n    let OrphanNICsAsPrivateSource = OrphanNICs\n    | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.SourceAddress\n    | extend NICState = 'orphan', IPAddress = PrivateIPAddress_s;\n    let OrphanNICsAsPublicSource = OrphanNICs\n    | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.SourceAddress\n    | extend NICState = 'orphan', IPAddress = PublicIPAddress;\n    let OrphanNICsAsPrivateDestination = OrphanNICs\n    | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.DestinationAddress\n    | extend NICState = 'orphan', IPAddress = PrivateIPAddress_s;\n    let OrphanNICsAsPublicDestination = OrphanNICs\n    | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.DestinationAddress\n    | extend NICState = 'orphan', IPAddress = PublicIPAddress;\n    let RemovedNICsAsPrivateSource = RemovedNICs\n    | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.SourceAddress\n    | extend NICState = 'inexisting', IPAddress = PrivateIPAddress_s;\n    let RemovedNICsAsPublicSource = RemovedNICs\n    | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.SourceAddress\n    | extend NICState = 'inexisting', IPAddress = PublicIPAddress;\n    let RemovedNICsAsPrivateDestination = RemovedNICs\n    | join kind=inner ( NSGRules ) on `$left.PrivateIPAddress_s == `$right.DestinationAddress\n    | extend NICState = 'inexisting', IPAddress = PrivateIPAddress_s;\n    let RemovedNICsAsPublicDestination = RemovedNICs\n    | join kind=inner ( NSGRules ) on `$left.PublicIPAddress == `$right.DestinationAddress\n    | extend NICState = 'inexisting', IPAddress = PublicIPAddress;\n    OrphanNICsAsPrivateSource\n    | union OrphanNICsAsPublicSource\n    | union OrphanNICsAsPrivateDestination\n    | union OrphanNICsAsPublicDestination\n    | union RemovedNICsAsPrivateSource\n    | union RemovedNICsAsPublicSource\n    | union RemovedNICsAsPrivateDestination\n    | union RemovedNICsAsPublicDestination\n    | where isnotempty(IPAddress)\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n    | distinct NSGId, NSGName, RuleName_s, SubscriptionGuid_g, SubscriptionName, ResourceGroupName_s, TenantGuid_g, Cloud_s, NICId, IPAddress, NICState, Tags_s\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.NSGId\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"nicId\"] = $result.NICId\n    $additionalInfoDictionary[\"ipAddress\"] = $result.IPAddress\n    $additionalInfoDictionary[\"nicState\"] = $result.NICState\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Security\"\n        ImpactedArea = \"Microsoft.Network/networkSecurityGroups\"\n        Impact = \"Medium\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"NSGRuleForOrphanOrInexistingNIC\"\n        RecommendationSubTypeId = \"3dc1d1f8-19ef-4572-9c9d-78d62831f55a\"\n        RecommendationDescription = \"NSG rules referring to orphan or inexisting NICs\"\n        RecommendationAction = \"Update or remove the NSG rule to improve your network security posture\"\n        InstanceId = $result.NSGId\n        InstanceName = \"$($result.NSGName)/$($result.RuleName_s)\"\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"nsgrules-orphaninexistingnics-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for NSG rules referring orphan or removed Public IPs...\"\n\n$baseQuery = @\"\n    let PIPsToday = materialize( $publicIpsTableName\n    | where TimeGenerated > ago(1d)\n    | extend PublicIPId = tolower(InstanceId_s)\n    | distinct PublicIPId, AssociatedResourceId_s, AllocationMethod_s, IPAddress );\n    let PIPsBefore = materialize( $publicIpsTableName\n    | where TimeGenerated < ago(1d)\n    | extend PublicIPId = tolower(InstanceId_s)\n    | distinct PublicIPId, IPAddress );\n    let OrphanStaticPIPs = PIPsToday\n    | where isempty(AssociatedResourceId_s) and AllocationMethod_s == 'static';\n    let OrphanDynamicPIPIDs = PIPsToday\n    | where isempty(AssociatedResourceId_s) and AllocationMethod_s == 'dynamic'\n    | distinct PublicIPId;\n    let PIPsTodayIds = PIPsToday | distinct PublicIPId;\n    let PIPsTodayIPs = PIPsToday | distinct IPAddress;\n    let OrphanDynamicPIPs = PIPsBefore\n    | where PublicIPId in (OrphanDynamicPIPIDs) and isnotempty(IPAddress) and IPAddress !in (PIPsTodayIPs);\n    let RemovedPIPs = PIPsBefore \n    | where PublicIPId !in (PIPsTodayIds) and isnotempty(IPAddress) and IPAddress !in (PIPsTodayIPs);\n    let NSGRules = materialize( $nsgRulesTableName\n    | where TimeGenerated > ago(1d)\n    | extend SourceAddresses = split(RuleSourceAddresses_s,',')\n    | mvexpand SourceAddresses\n    | extend SourceAddress = replace('/32','',tostring(SourceAddresses))\n    | extend DestinationAddresses = split(RuleDestinationAddresses_s,',')\n    | mvexpand DestinationAddresses\n    | extend DestinationAddress = replace('/32','',tostring(DestinationAddresses))\n    | project NSGId = InstanceId_s, RuleName_s, DestinationAddress, SourceAddress, SubscriptionGuid_g, Cloud_s, TenantGuid_g, ResourceGroupName_s, NSGName = NSGName_s, Tags_s);\n    let OrphanStaticPIPsAsSource = OrphanStaticPIPs\n    | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.SourceAddress\n    | extend PIPState = 'orphan';\n    let OrphanStaticPIPsAsDestination = OrphanStaticPIPs\n    | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.DestinationAddress\n    | extend PIPState = 'orphan';\n    let OrphanDynamicPIPsAsSource = OrphanDynamicPIPs\n    | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.SourceAddress\n    | extend PIPState = 'orphan';\n    let OrphanDynamicPIPsAsDestination = OrphanDynamicPIPs\n    | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.DestinationAddress\n    | extend PIPState = 'orphan';\n    let RemovedPIPsAsSource = RemovedPIPs\n    | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.SourceAddress\n    | extend PIPState = 'inexisting';\n    let RemovedPIPsAsDestination = RemovedPIPs\n    | join kind=inner ( NSGRules ) on `$left.IPAddress == `$right.DestinationAddress\n    | extend PIPState = 'inexisting';\n    OrphanStaticPIPsAsSource\n    | union OrphanDynamicPIPsAsSource\n    | union OrphanStaticPIPsAsDestination\n    | union OrphanDynamicPIPsAsDestination\n    | union RemovedPIPsAsSource\n    | union RemovedPIPsAsDestination\n    | join kind=leftouter ( \n        $subscriptionsTableName \n        | where TimeGenerated > ago(1d)\n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n    | distinct NSGId, NSGName, RuleName_s, SubscriptionGuid_g, SubscriptionName, ResourceGroupName_s, TenantGuid_g, Cloud_s, PublicIPId, IPAddress, PIPState, AllocationMethod_s, Tags_s\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    switch ($result.Cloud_s)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n\n    $queryInstanceId = $result.NSGId\n    $detailsURL = \"https://portal.azure.$azureTld/#@$($result.TenantGuid_g)/resource/$queryInstanceId/overview\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"publicIPId\"] = $result.PublicIPId\n    $additionalInfoDictionary[\"ipAddress\"] = $result.IPAddress\n    $additionalInfoDictionary[\"publicIPState\"] = $result.PIPState\n    $additionalInfoDictionary[\"allocationMethod\"] = $result.AllocationMethod_s\n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Security\"\n        ImpactedArea = \"Microsoft.Network/networkSecurityGroups\"\n        Impact = \"High\"\n        RecommendationType = \"BestPractices\"\n        RecommendationSubType = \"NSGRuleForOrphanOrInexistingPublicIP\"\n        RecommendationSubTypeId = \"fe40cbe7-bdee-4cce-b072-cf25e1247b7a\"\n        RecommendationDescription = \"NSG rules referring to orphan or inexisting Public IPs\"\n        RecommendationAction = \"Update or remove the NSG rule to improve your network security posture\"\n        InstanceId = $result.NSGId\n        InstanceName = \"$($result.NSGName)/$($result.RuleName_s)\"\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"nsgrules-orphaninexistingpublicips-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nWrite-Output \"Looking for orphaned Public IPs...\"\n\n$baseQuery = @\"\n    let interval = 30d;\n    let etime = todatetime(toscalar($consumptionTableName | where todatetime(Date_s) < now() and todatetime(Date_s) > ago(30d) | summarize max(todatetime(Date_s)))); \n    let stime = etime-interval;     \n    $publicIpsTableName\n    | where TimeGenerated > ago(1d) and isempty(AssociatedResourceId_s)\n    | distinct Name_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, AllocationMethod_s, Tags_s, Cloud_s\n    | join kind=leftouter (\n        $consumptionTableName\n        | where todatetime(Date_s) between (stime..etime)\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize Last30DaysCost=sum(todouble(CostInBillingCurrency_s)) by Name_s, InstanceId_s, SubscriptionGuid_g, TenantGuid_g, ResourceGroupName_s, SkuName_s, AllocationMethod_s, Tags_s, Cloud_s    \n    | join kind=leftouter ( \n        $subscriptionsTableName\n        | where TimeGenerated > ago(1d) \n        | where ContainerType_s =~ 'microsoft.resources/subscriptions' \n        | project SubscriptionGuid_g, SubscriptionName = ContainerName_s \n    ) on SubscriptionGuid_g\n\"@\n\ntry\n{\n    $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceId -Query $baseQuery -Timespan (New-TimeSpan -Days $recommendationSearchTimeSpan) -Wait 600 -IncludeStatistics\n    if ($queryResults)\n    {\n        $results = [System.Linq.Enumerable]::ToArray($queryResults.Results)        \n    }\n}\ncatch\n{\n    Write-Warning -Message \"Query failed. Debug the following query in the AOE Log Analytics workspace: $baseQuery\"    \n    Write-Warning -Message $error[0]\n    $recommendationsErrors++\n}\n\nWrite-Output \"Query finished with $($results.Count) results.\"\n\nWrite-Output \"Query statistics: $($queryResults.Statistics.query)\"\n\n# Build the recommendations objects\n\n$recommendations = @()\n$datetime = (get-date).ToUniversalTime()\n$timestamp = $datetime.ToString(\"yyyy-MM-ddTHH:mm:00.000Z\")\n\nforeach ($result in $results)\n{\n    $queryInstanceId = $result.InstanceId_s\n    $queryText = @\"\n    $publicIpsTableName\n    | where InstanceId_s == '$queryInstanceId' and isempty(AssociatedResourceId_s)\n    | distinct InstanceId_s, Name_s, AllocationMethod_s, SkuName_s, TimeGenerated\n    | summarize LastAttachedDate = min(TimeGenerated) by InstanceId_s, Name_s, AllocationMethod_s, SkuName_s\n    | join kind=leftouter (\n        $consumptionTableName\n        | project InstanceId_s=tolower(ResourceId), CostInBillingCurrency_s, Date_s\n    ) on InstanceId_s\n    | summarize CostsSinceDetached = sumif(todouble(CostInBillingCurrency_s), todatetime(Date_s) > LastAttachedDate) by Name_s, LastAttachedDate, AllocationMethod_s, SkuName_s\n\"@\n    $encodedQuery = [System.Uri]::EscapeDataString($queryText)\n    $detailsQueryStart = $deploymentDate\n    $detailsQueryEnd = $datetime.AddDays(8).ToString(\"yyyy-MM-dd\")\n    switch ($cloudEnvironment)\n    {\n        \"AzureCloud\" { $azureTld = \"com\" }\n        \"AzureChinaCloud\" { $azureTld = \"cn\" }\n        \"AzureUSGovernment\" { $azureTld = \"us\" }\n        default { $azureTld = \"com\" }\n    }\n    $detailsURL = \"https://portal.azure.$azureTld#@$workspaceTenantId/blade/Microsoft_Azure_Monitoring_Logs/LogsBlade/resourceId/%2Fsubscriptions%2F$workspaceSubscriptionId%2Fresourcegroups%2F$workspaceRG%2Fproviders%2Fmicrosoft.operationalinsights%2Fworkspaces%2F$workspaceName/source/LogsBlade.AnalyticsShareLinkToQuery/query/$encodedQuery/timespan/$($detailsQueryStart)T00%3A00%3A00.000Z%2F$($detailsQueryEnd)T00%3A00%3A00.000Z\"\n\n    $additionalInfoDictionary = @{}\n\n    $additionalInfoDictionary[\"currentSku\"] = $result.SkuName_s\n    $additionalInfoDictionary[\"allocationMethod\"] = $result.AllocationMethod_s\n    $additionalInfoDictionary[\"CostsAmount\"] = [double] $result.Last30DaysCost \n    $additionalInfoDictionary[\"savingsAmount\"] = [double] $result.Last30DaysCost \n\n    $fitScore = 5\n\n    $tags = @{}\n\n    if (-not([string]::IsNullOrEmpty($result.Tags_s)))\n    {\n        $tagPairs = $result.Tags_s.Substring(2, $result.Tags_s.Length - 3).Split(';')\n        foreach ($tagPairString in $tagPairs)\n        {\n            $tagPair = $tagPairString.Split('=')\n            if (-not([string]::IsNullOrEmpty($tagPair[0])) -and -not([string]::IsNullOrEmpty($tagPair[1])))\n            {\n                $tagName = $tagPair[0].Trim()\n                $tagValue = $tagPair[1].Trim()\n                $tags[$tagName] = $tagValue    \n            }\n        }\n    }\n\n    $recommendation = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $result.Cloud_s\n        Category = \"Cost\"\n        ImpactedArea = \"Microsoft.Network/publicIPAddresses\"\n        Impact = \"Low\"\n        RecommendationType = \"Saving\"\n        RecommendationSubType = \"OrphanedPublicIP\"\n        RecommendationSubTypeId = \"3125883f-8b9f-4bde-a0ff-6c739858c6e1\"\n        RecommendationDescription = \"Orphaned Public IP (without owner resource) incur in unnecessary costs\"\n        RecommendationAction = \"Delete the Public IP or change its configuration to dynamic allocation\"\n        InstanceId = $result.InstanceId_s\n        InstanceName = $result.Name_s\n        AdditionalInfo = $additionalInfoDictionary\n        ResourceGroup = $result.ResourceGroupName_s\n        SubscriptionGuid = $result.SubscriptionGuid_g\n        SubscriptionName = $result.SubscriptionName\n        TenantGuid = $result.TenantGuid_g\n        FitScore = $fitScore\n        Tags = $tags\n        DetailsURL = $detailsURL\n    }\n\n    $recommendations += $recommendation\n}\n\n# Export the recommendations as JSON to blob storage\n\n$fileDate = $datetime.ToString(\"yyyy-MM-dd\")\n$jsonExportPath = \"orphanedpublicips-$fileDate.json\"\n$recommendations | ConvertTo-Json | Out-File $jsonExportPath\n\n$jsonBlobName = $jsonExportPath\n$jsonProperties = @{\"ContentType\" = \"application/json\"};\nSet-AzStorageBlobContent -File $jsonExportPath -Container $storageAccountSinkContainer -Properties $jsonProperties -Blob $jsonBlobName -Context $sa.Context -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Uploaded $jsonBlobName to Blob Storage...\"\n\nRemove-Item -Path $jsonExportPath -Force\n\n$now = (Get-Date).ToUniversalTime().ToString(\"yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'\")\nWrite-Output \"[$now] Removed $jsonExportPath from local disk...\"\n\nif ($recommendationsErrors -gt 0)\n{\n    throw \"Some of the recommendations queries failed. Please, review the job logs for additional information.\"\n}"
  },
  {
    "path": "runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RemediationLogsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"remediationlogs\"\n}\n\n$minFitScore = [double] (Get-AutomationVariable -Name  \"AzureOptimization_RemediateRightSizeMinFitScore\" -ErrorAction SilentlyContinue)\nif (-not($minFitScore -gt 0.0)) {\n    $minFitScore = 5.0\n}\n\n$minWeeksInARow = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RemediateRightSizeMinWeeksInARow\" -ErrorAction SilentlyContinue)\nif (-not($minWeeksInARow -gt 0)) {\n    $minWeeksInARow = 4\n}\n\n$tagsFilter = Get-AutomationVariable -Name  \"AzureOptimization_RemediateRightSizeTagsFilter\" -ErrorAction SilentlyContinue\n# example: '[ { \"tagName\": \"a\", \"tagValue\": \"b\" }, { \"tagName\": \"c\", \"tagValue\": \"d\" } ]'\nif (-not($tagsFilter)) {\n    $tagsFilter = '{}'\n}\n$tagsFilter = $tagsFilter | ConvertFrom-Json\n\n$rightSizeRecommendationId = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationAdvisorCostRightSizeId\" -ErrorAction SilentlyContinue\nif (-not($rightSizeRecommendationId)) {\n    $rightSizeRecommendationId = 'e10b1381-5f0a-47ff-8c7b-37bd13d7c974'\n}\n\n$SqlTimeout = 0\n$recommendationsTable = \"Recommendations\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n# get reference to storage sink\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n\nWrite-Output \"Querying for right-size recommendations with fit score >= $minFitScore made consecutively for the last $minWeeksInARow weeks.\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = @\"\n        SELECT InstanceId, Cloud, TenantGuid, JSON_VALUE(AdditionalInfo, '`$.currentSku') AS CurrentSKU, JSON_VALUE(AdditionalInfo, '`$.targetSku') AS TargetSKU, COUNT(InstanceId)\n        FROM [dbo].[$recommendationsTable] \n        WHERE RecommendationSubTypeId = '$rightSizeRecommendationId' AND FitScore >= $minFitScore AND GeneratedDate >= GETDATE()-(7*$minWeeksInARow)\n        GROUP BY InstanceId, Cloud, TenantGuid, JSON_VALUE(AdditionalInfo, '`$.currentSku'), JSON_VALUE(AdditionalInfo, '`$.targetSku')\n        HAVING COUNT(InstanceId) >= $minWeeksInARow\n\"@    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $vmsToRightSize = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($vmsToRightSize) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\nWrite-Output \"Found $($vmsToRightSize.Rows.Count) remediation opportunities.\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$logEntries = @()\n\n$datetime = (get-date).ToUniversalTime()\n$hour = $datetime.Hour\n$min = $datetime.Minute\n$timestamp = $datetime.ToString(\"yyyy-MM-ddT$($hour):$($min):00.000Z\")\n\n$ctx = Get-AzContext\n\nforeach ($vm in $vmsToRightSize.Rows)\n{\n    $isEligible = $false\n    $logDetails = $null\n    if ([string]::IsNullOrEmpty($tagsFilter))\n    {\n        $isEligible = $true\n    }\n    else\n    {\n        $vmTags = Get-AzTag -ResourceId $vm.InstanceId -ErrorAction SilentlyContinue\n        if ($vmTags)\n        {\n            foreach ($tagFilter in $tagsFilter)\n            {\n                if ($vmTags.Properties.TagsProperty.($tagFilter.tagName) -eq $tagFilter.tagValue)\n                {\n                    $isEligible = $true\n                }\n                else\n                {\n                    $isEligible = $false\n                    break\n                }\n            }\n        }\n    }\n\n    $subscriptionId = $vm.InstanceId.Split(\"/\")[2]\n    $resourceGroup = $vm.InstanceId.Split(\"/\")[4]\n    $instanceName = $vm.InstanceId.Split(\"/\")[8]\n    \n    if ($isEligible)\n    {\n        Write-Output \"Downsizing (SIMULATE=$Simulate) $($vm.InstanceId) to $($vm.TargetSKU)...\"\n        if (-not($Simulate) -and $ctx.Environment.Name -eq $vm.Cloud -and $ctx.Tenant.Id -eq $vm.TenantGuid)\n        {\n            if ($ctx.Subscription.Id -ne $subscriptionId)\n            {\n                Select-AzSubscription -SubscriptionId $subscriptionId | Out-Null\n                $ctx = Get-AzContext\n            }\n            $vmObj = Get-AzVM -ResourceGroupName $resourceGroup -VMName $instanceName -ErrorAction SilentlyContinue\n            if ($vmObj)\n            {\n                $vmObj.HardwareProfile.VmSize = $vm.TargetSKU\n                Update-AzVM -VM $vmObj -ResourceGroupName $resourceGroup    \n            }\n            else\n            {\n                Write-Output \"Skipping as VM was already removed.\"                \n            }\n        }\n        else\n        {\n            Write-Output \"Did not apply remediation.\"    \n        }\n    }\n\n    $logDetails = @{\n        IsEligible = $isEligible\n        CurrentSku = $vm.CurrentSKU\n        TargetSku = $vm.TargetSKU\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $vm.Cloud\n        TenantGuid = $vm.TenantGuid\n        SubscriptionGuid = $subscriptionId\n        ResourceGroupName = $resourceGroup.ToLower()\n        InstanceName = $instanceName.ToLower()\n        InstanceId = $vm.InstanceId.ToLower()\n        Simulate = $Simulate\n        LogDetails = $logDetails | ConvertTo-Json -Compress\n        RecommendationSubTypeId = $rightSizeRecommendationId\n    }\n    \n    $logEntries += $logentry\n}\n    \n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-rightsizefiltered.csv\"\n\n$logEntries | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n"
  },
  {
    "path": "runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RemediationLogsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"remediationlogs\"\n}\n\n$minFitScore = [double] (Get-AutomationVariable -Name  \"AzureOptimization_RemediateLongDeallocatedVMsMinFitScore\" -ErrorAction SilentlyContinue)\nif (-not($minFitScore -gt 0.0)) {\n    $minFitScore = 5.0\n}\n\n$minWeeksInARow = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow\" -ErrorAction SilentlyContinue)\nif (-not($minWeeksInARow -gt 0)) {\n    $minWeeksInARow = 4\n}\n\n$tagsFilter = Get-AutomationVariable -Name  \"AzureOptimization_RemediateLongDeallocatedVMsTagsFilter\" -ErrorAction SilentlyContinue\n# example: '[ { \"tagName\": \"a\", \"tagValue\": \"b\" }, { \"tagName\": \"c\", \"tagValue\": \"d\" } ]'\nif (-not($tagsFilter)) {\n    $tagsFilter = '{}'\n}\n$tagsFilter = $tagsFilter | ConvertFrom-Json\n\n$recommendationId = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationLongDeallocatedVMsId\" -ErrorAction SilentlyContinue\nif (-not($recommendationId)) {\n    $recommendationId = 'c320b790-2e58-452a-aa63-7b62c383ad8a'\n}\n\n$SqlTimeout = 0\n$recommendationsTable = \"Recommendations\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n# get reference to storage sink\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n\nWrite-Output \"Querying for long-deallocated recommendations with fit score >= $minFitScore made consecutively for the last $minWeeksInARow weeks.\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = @\"\n        SELECT InstanceId, Cloud, TenantGuid, COUNT(InstanceId)\n        FROM [dbo].[$recommendationsTable] \n        WHERE RecommendationSubTypeId = '$recommendationId' AND FitScore >= $minFitScore AND GeneratedDate >= GETDATE()-(7*$minWeeksInARow)\n        GROUP BY InstanceId, Cloud, TenantGuid\n        HAVING COUNT(InstanceId) >= $minWeeksInARow\n\"@    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $deallocatedVMs = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($deallocatedVMs) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\nWrite-Output \"Found $($deallocatedVMs.Rows.Count) remediation opportunities.\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$logEntries = @()\n\n$datetime = (get-date).ToUniversalTime()\n$hour = $datetime.Hour\n$min = $datetime.Minute\n$timestamp = $datetime.ToString(\"yyyy-MM-ddT$($hour):$($min):00.000Z\")\n\n$ctx = Get-AzContext\n\nforeach ($vm in $deallocatedVMs.Rows)\n{\n    $isEligible = $false\n    $logDetails = $null\n    if ([string]::IsNullOrEmpty($tagsFilter))\n    {\n        $isEligible = $true\n    }\n    else\n    {\n        $vmTags = Get-AzTag -ResourceId $vm.InstanceId -ErrorAction SilentlyContinue\n        if ($vmTags)\n        {\n            foreach ($tagFilter in $tagsFilter)\n            {\n                if ($vmTags.Properties.TagsProperty.($tagFilter.tagName) -eq $tagFilter.tagValue)\n                {\n                    $isEligible = $true\n                }\n                else\n                {\n                    $isEligible = $false\n                    break\n                }\n            }\n        }\n    }\n\n    $subscriptionId = $vm.InstanceId.Split(\"/\")[2]\n    $resourceGroup = $vm.InstanceId.Split(\"/\")[4]\n    $instanceName = $vm.InstanceId.Split(\"/\")[8]\n    \n    if ($isEligible)\n    {\n        $vmState = \"Unknown\"\n        $hasManagedDisks = $false\n        $osDiskSkuName = \"Unknown\"\n        $dataDisksSkuNames = \"Unknown\"\n\n        Write-Output \"Downsizing (SIMULATE=$Simulate) $($vm.InstanceId) disks to Standard_LRS...\"\n        if ($ctx.Environment.Name -eq $vm.Cloud -and $ctx.Tenant.Id -eq $vm.TenantGuid)\n        {\n            if ($ctx.Subscription.Id -ne $subscriptionId)\n            {\n                Select-AzSubscription -SubscriptionId $subscriptionId | Out-Null\n                $ctx = Get-AzContext\n            }\n            $vmObj = Get-AzVM -ResourceGroupName $resourceGroup -VMName $instanceName -Status -ErrorAction SilentlyContinue\n            if ($vmObj.PowerState -eq 'VM deallocated')\n            {\n                $vmState = \"Deallocated\"\n                $osDiskId = $vmObj.StorageProfile.OsDisk.ManagedDisk.Id\n                $dataDiskIds = $vmObj.StorageProfile.DataDisks.ManagedDisk.Id\n                if ($osDiskId)\n                {\n                    $hasManagedDisks = $true\n                    $disk = Get-AzDisk -ResourceGroupName $osDiskId.Split(\"/\")[4] -DiskName $osDiskId.Split(\"/\")[8]\n                    $osDiskSkuName = $disk.Sku.Name\n                    if (-not($Simulate) -and $disk.Sku.Name -ne 'Standard_LRS')\n                    {\n                        $disk.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Standard_LRS')\n                        $disk | Update-AzDisk | Out-Null\n                    }\n                    else\n                    {\n                        Write-Output \"Skipping as OS disk is already HDD.\"                        \n                    }\n                    foreach ($dataDiskId in $dataDiskIds)\n                    {\n                        $disk = Get-AzDisk -ResourceGroupName $dataDiskId.Split(\"/\")[4] -DiskName $dataDiskId.Split(\"/\")[8]\n                        if ($dataDisksSkuNames -eq 'Unknown')\n                        {\n                            $dataDisksSkuNames = $disk.Sku.Name\n                        }\n                        else\n                        {\n                            if ($dataDisksSkuNames -notlike \"*$($disk.Sku.Name)*\")\n                            {\n                                $dataDisksSkuNames += \",$($disk.Sku.Name)\"\n                            }\n                        }\n                        \n                        if (-not($Simulate) -and $disk.Sku.Name -ne 'Standard_LRS')\n                        {\n                            $disk.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Standard_LRS')\n                            $disk | Update-AzDisk | Out-Null\n                        }\n                        else\n                        {\n                            Write-Output \"Skipping as Data disk is already HDD.\"                        \n                        }                            \n                    }\n                }\n                else\n                {\n                    Write-Output \"Skipping as disks are not Managed Disks.\"    \n                    $hasManagedDisks = $false\n                }\n            }\n            else\n            {\n                if ($vmObj)\n                {\n                    Write-Output \"Skipping as VM is not deallocated.\"    \n                    $vmState = \"Running\"\n                }\n                else\n                {\n                    Write-Output \"Skipping as VM was already removed.\"    \n                    $vmState = \"Removed\"                        \n                }\n            }\n        }\n        else\n        {\n            Write-Output \"Could not apply remediation as VM is in another cloud/tenant.\"    \n        }\n    }\n\n    $logDetails = @{\n        IsEligible = $isEligible\n        VMState = $vmState\n        HasManagedDisks = $hasManagedDisks\n        OsDiskSkuName = $osDiskSkuName\n        DataDisksSkuName = $dataDisksSkuNames\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $vm.Cloud\n        TenantGuid = $vm.TenantGuid\n        SubscriptionGuid = $subscriptionId\n        ResourceGroupName = $resourceGroup.ToLower()\n        InstanceName = $instanceName.ToLower()\n        InstanceId = $vm.InstanceId.ToLower()\n        Simulate = $Simulate\n        LogDetails = $logDetails | ConvertTo-Json -Compress\n        RecommendationSubTypeId = $recommendationId\n    }\n    \n    $logEntries += $logentry\n}\n    \n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-longdeallocatedvmsfiltered.csv\"\n\n$logEntries | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n"
  },
  {
    "path": "runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1",
    "content": "param(\n    [Parameter(Mandatory = $false)]\n    [bool] $Simulate = $true\n)\n\n$ErrorActionPreference = \"Stop\"\n\n$cloudEnvironment = Get-AutomationVariable -Name \"AzureOptimization_CloudEnvironment\" -ErrorAction SilentlyContinue # AzureCloud|AzureChinaCloud\nif ([string]::IsNullOrEmpty($cloudEnvironment))\n{\n    $cloudEnvironment = \"AzureCloud\"\n}\n$authenticationOption = Get-AutomationVariable -Name  \"AzureOptimization_AuthenticationOption\" -ErrorAction SilentlyContinue # ManagedIdentity|UserAssignedManagedIdentity\nif ([string]::IsNullOrEmpty($authenticationOption))\n{\n    $authenticationOption = \"ManagedIdentity\"\n}\nif ($authenticationOption -eq \"UserAssignedManagedIdentity\")\n{\n    $uamiClientID = Get-AutomationVariable -Name \"AzureOptimization_UAMIClientID\"\n}\n\n$sqlserver = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerHostname\"\n$sqlserverCredential = Get-AutomationPSCredential -Name \"AzureOptimization_SQLServerCredential\"\n$SqlUsername = $sqlserverCredential.UserName \n$SqlPass = $sqlserverCredential.GetNetworkCredential().Password \n$sqldatabase = Get-AutomationVariable -Name  \"AzureOptimization_SQLServerDatabase\" -ErrorAction SilentlyContinue\nif ([string]::IsNullOrEmpty($sqldatabase))\n{\n    $sqldatabase = \"azureoptimization\"\n}\n$storageAccountSink = Get-AutomationVariable -Name  \"AzureOptimization_StorageSink\"\n$storageAccountSinkRG = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkRG\"\n$storageAccountSinkSubscriptionId = Get-AutomationVariable -Name  \"AzureOptimization_StorageSinkSubId\"\n$storageAccountSinkContainer = Get-AutomationVariable -Name  \"AzureOptimization_RemediationLogsContainer\" -ErrorAction SilentlyContinue \nif ([string]::IsNullOrEmpty($storageAccountSinkContainer)) {\n    $storageAccountSinkContainer = \"remediationlogs\"\n}\n\n$minFitScore = [double] (Get-AutomationVariable -Name  \"AzureOptimization_RemediateUnattachedDisksMinFitScore\" -ErrorAction SilentlyContinue)\nif (-not($minFitScore -gt 0.0)) {\n    $minFitScore = 5.0\n}\n\n$minWeeksInARow = [int] (Get-AutomationVariable -Name  \"AzureOptimization_RemediateUnattachedDisksMinWeeksInARow\" -ErrorAction SilentlyContinue)\nif (-not($minWeeksInARow -gt 0)) {\n    $minWeeksInARow = 4\n}\n\n$tagsFilter = Get-AutomationVariable -Name  \"AzureOptimization_RemediateUnattachedDisksTagsFilter\" -ErrorAction SilentlyContinue\n# example: '[ { \"tagName\": \"a\", \"tagValue\": \"b\" }, { \"tagName\": \"c\", \"tagValue\": \"d\" } ]'\nif (-not($tagsFilter)) {\n    $tagsFilter = '{}'\n}\n$tagsFilter = $tagsFilter | ConvertFrom-Json\n\n$remediationAction = Get-AutomationVariable -Name  \"AzureOptimization_RemediateUnattachedDisksAction\" -ErrorAction SilentlyContinue # Delete / Downsize\nif (-not($remediationAction)) {\n    $remediationAction = \"Delete\"\n}\n\n$recommendationId = Get-AutomationVariable -Name  \"AzureOptimization_RecommendationUnattachedDisksId\" -ErrorAction SilentlyContinue\nif (-not($recommendationId)) {\n    $recommendationId = 'c84d5e86-e2d6-4d62-be7c-cecfbd73b0db'\n}\n\n$SqlTimeout = 0\n$recommendationsTable = \"Recommendations\"\n\n\"Logging in to Azure with $authenticationOption...\"\n\nswitch ($authenticationOption) {\n    \"UserAssignedManagedIdentity\" { \n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment -AccountId $uamiClientID\n        break\n    }\n    Default { #ManagedIdentity\n        Connect-AzAccount -Identity -EnvironmentName $cloudEnvironment \n        break\n    }\n}\n\n# get reference to storage sink\nSelect-AzSubscription -SubscriptionId $storageAccountSinkSubscriptionId\n$saCtx = (Get-AzStorageAccount -ResourceGroupName $storageAccountSinkRG -Name $storageAccountSink).Context\n\nWrite-Output \"Querying for unattached disks recommendations with fit score >= $minFitScore made consecutively for the last $minWeeksInARow weeks.\"\n\n$tries = 0\n$connectionSuccess = $false\ndo {\n    $tries++\n    try {\n        $Conn = New-Object System.Data.SqlClient.SqlConnection(\"Server=tcp:$sqlserver,1433;Database=$sqldatabase;User ID=$SqlUsername;Password=$SqlPass;Trusted_Connection=False;Encrypt=True;Connection Timeout=$SqlTimeout;\") \n        $Conn.Open() \n        $Cmd=new-object system.Data.SqlClient.SqlCommand\n        $Cmd.Connection = $Conn\n        $Cmd.CommandTimeout = $SqlTimeout\n        $Cmd.CommandText = @\"\n        SELECT InstanceId, Cloud, TenantGuid, COUNT(InstanceId)\n        FROM [dbo].[$recommendationsTable] \n        WHERE RecommendationSubTypeId = '$recommendationId' AND FitScore >= $minFitScore AND GeneratedDate >= GETDATE()-(7*$minWeeksInARow)\n        GROUP BY InstanceId, Cloud, TenantGuid\n        HAVING COUNT(InstanceId) >= $minWeeksInARow\n\"@    \n        $sqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter\n        $sqlAdapter.SelectCommand = $Cmd\n        $unattachedDisks = New-Object System.Data.DataTable\n        $sqlAdapter.Fill($unattachedDisks) | Out-Null            \n        $connectionSuccess = $true\n    }\n    catch {\n        Write-Output \"Failed to contact SQL at try $tries.\"\n        Write-Output $Error[0]\n        Start-Sleep -Seconds ($tries * 20)\n    }    \n} while (-not($connectionSuccess) -and $tries -lt 3)\n\nif (-not($connectionSuccess))\n{\n    throw \"Could not establish connection to SQL.\"\n}\n\nWrite-Output \"Found $($unattachedDisks.Rows.Count) remediation opportunities.\"\n\n$Conn.Close()    \n$Conn.Dispose()            \n\n$logEntries = @()\n\n$datetime = (get-date).ToUniversalTime()\n$hour = $datetime.Hour\n$min = $datetime.Minute\n$timestamp = $datetime.ToString(\"yyyy-MM-ddT$($hour):$($min):00.000Z\")\n\n$ctx = Get-AzContext\n\nforeach ($disk in $unattachedDisks.Rows)\n{\n    $isEligible = $false\n    $logDetails = $null\n    if ([string]::IsNullOrEmpty($tagsFilter))\n    {\n        $isEligible = $true\n    }\n    else\n    {\n        $diskTags = Get-AzTag -ResourceId $disk.InstanceId -ErrorAction SilentlyContinue\n        if ($diskTags)\n        {\n            foreach ($tagFilter in $tagsFilter)\n            {\n                if ($diskTags.Properties.TagsProperty.($tagFilter.tagName) -eq $tagFilter.tagValue)\n                {\n                    $isEligible = $true\n                }\n                else\n                {\n                    $isEligible = $false\n                    break\n                }\n            }\n        }\n    }\n\n    $subscriptionId = $disk.InstanceId.Split(\"/\")[2]\n    $resourceGroup = $disk.InstanceId.Split(\"/\")[4]\n    $instanceName = $disk.InstanceId.Split(\"/\")[8]\n    \n    if ($isEligible)\n    {\n        $diskState = \"Unknown\"\n        $currentSku = \"Unknown\"\n\n        Write-Output \"Performing $remediationAction action (SIMULATE=$Simulate) on $($disk.InstanceId) disk...\"\n        if ($ctx.Environment.Name -eq $disk.Cloud -and $ctx.Tenant.Id -eq $disk.TenantGuid)\n        {\n            if ($ctx.Subscription.Id -ne $subscriptionId)\n            {\n                Select-AzSubscription -SubscriptionId $subscriptionId | Out-Null\n                $ctx = Get-AzContext\n            }\n            $diskObj = Get-AzDisk -ResourceGroupName $resourceGroup -DiskName $instanceName -ErrorAction SilentlyContinue\n            if (-not($diskObj.ManagedBy))\n            {\n                $diskState = \"Unattached\"\n                $currentSku = $diskObj.Sku.Name\n                if ($remediationAction -eq \"Downsize\")\n                {\n                    if (-not($Simulate) -and $diskObj.Sku.Name -ne 'Standard_LRS')\n                    {\n                        $diskObj.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Standard_LRS')\n                        $diskObj | Update-AzDisk | Out-Null\n                    }\n                    else\n                    {\n                        Write-Output \"Skipping as disk is already HDD.\"                        \n                    }\n                }\n                elseif ($remediationAction -eq \"Delete\")\n                {\n                    if (-not($Simulate))\n                    {\n                        Remove-AzDisk -ResourceGroupName $resourceGroup -DiskName $instanceName -Force | Out-Null\n                    }\n                }\n                else\n                {\n                    Write-Output \"Skipping as action is not supported.\"\n                }\n            }\n            else\n            {\n                if ($diskObj)\n                {\n                    Write-Output \"Skipping as disk is not unattached.\"    \n                    $diskState = \"Attached\"    \n                }\n                else\n                {\n                    Write-Output \"Skipping as disk was already removed.\"    \n                    $diskState = \"Removed\"                        \n                }\n            }\n        }\n        else\n        {\n            Write-Output \"Could not apply remediation as disk is in another cloud/tenant.\"    \n        }\n    }\n\n    $logDetails = @{\n        IsEligible = $isEligible\n        RemediationAction = $remediationAction\n        DiskState = $diskState\n        CurrentSku = $currentSku\n    }\n\n    $logentry = New-Object PSObject -Property @{\n        Timestamp = $timestamp\n        Cloud = $disk.Cloud\n        TenantGuid = $disk.TenantGuid\n        SubscriptionGuid = $subscriptionId\n        ResourceGroupName = $resourceGroup.ToLower()\n        InstanceName = $instanceName.ToLower()\n        InstanceId = $disk.InstanceId.ToLower()\n        Simulate = $Simulate\n        LogDetails = $logDetails | ConvertTo-Json -Compress\n        RecommendationSubTypeId = $recommendationId\n    }\n    \n    $logEntries += $logentry\n}\n    \n$today = $datetime.ToString(\"yyyyMMdd\")\n$csvExportPath = \"$today-unattacheddisksfiltered.csv\"\n\n$logEntries | Export-Csv -Path $csvExportPath -NoTypeInformation\n\n$csvBlobName = $csvExportPath\n\n$csvProperties = @{\"ContentType\" = \"text/csv\"};\n\nSet-AzStorageBlobContent -File $csvExportPath -Container $storageAccountSinkContainer -Properties $csvProperties -Blob $csvBlobName -Context $saCtx -Force\n"
  },
  {
    "path": "upgrade-manifest.json",
    "content": "{\n    \"modules\": [\n        {\n            \"name\": \"Az.Accounts\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.Accounts/2.12.1\"\n        },\n        {\n            \"name\": \"Microsoft.Graph.Authentication\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Authentication/2.4.0\"\n        },\n        {\n            \"name\": \"Az.Compute\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.Compute/5.7.0\"\n        },\n        {\n            \"name\": \"Az.OperationalInsights\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.OperationalInsights/3.2.0\"\n        },\n        {\n            \"name\": \"Az.ResourceGraph\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.ResourceGraph/0.13.0\"\n        },\n        {\n            \"name\": \"Az.Storage\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.Storage/5.5.0\"\n        },\n        {\n            \"name\": \"Az.Resources\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.Resources/6.6.0\"\n        },\n        {\n            \"name\": \"Az.Monitor\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.Monitor/4.4.1\"\n        },\n        {\n            \"name\": \"Az.PolicyInsights\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Az.PolicyInsights/1.6.0\"\n        },\n        {\n            \"name\": \"Microsoft.Graph.Users\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Users/2.4.0\"\n        },\n        {\n            \"name\": \"Microsoft.Graph.Groups\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Groups/2.4.0\"\n        },\n        {\n            \"name\": \"Microsoft.Graph.Applications\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Applications/2.4.0\"\n        },\n        {\n            \"name\": \"Microsoft.Graph.Identity.DirectoryManagement\",\n            \"url\": \"https://www.powershellgallery.com/api/v2/package/Microsoft.Graph.Identity.DirectoryManagement/2.4.0\"\n        }\n    ],\n    \"schedules\": [\n        {\n            \"name\": \"AzureOptimization_ExportAADObjectsDaily\",\n            \"offset\": \"PT1H\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestAADObjectsDaily\",\n            \"offset\": \"PT2H\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportAdvisorWeekly\",\n            \"offset\": \"PT1H15M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestAdvisorWeekly\",\n            \"offset\": \"PT1H45M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportARGDaily\",\n            \"offset\": \"PT1H05M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportPolicyStateDaily\",\n            \"offset\": \"PT1H\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGAppGWsDaily\",\n            \"offset\": \"PT1H30M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGAvailSetsDaily\",\n            \"offset\": \"PT1H30M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGLoadBalancersDaily\",\n            \"offset\": \"PT1H30M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGDisksDaily\",\n            \"offset\": \"PT1H30M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGPublicIPsDaily\",\n            \"offset\": \"PT1H31M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGNICsDaily\",\n            \"offset\": \"PT1H31M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGNSGsDaily\",\n            \"offset\": \"PT1H31M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGVNetsDaily\",\n            \"offset\": \"PT1H31M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGVHDsDaily\",\n            \"offset\": \"PT1H32M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGVMsDaily\",\n            \"offset\": \"PT1H32M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGVMSSDaily\",\n            \"offset\": \"PT1H32M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGSqlDbDaily\",\n            \"offset\": \"PT1H32M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGAppServicePlanDaily\",\n            \"offset\": \"PT1H33M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestPolicyStateDaily\",\n            \"offset\": \"PT1H33M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestARGResourceContainersDaily\",\n            \"offset\": \"PT1H33M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportConsumptionDaily\",\n            \"offset\": \"PT1H\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestConsumptionDaily\",\n            \"offset\": \"PT2H\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportRBACDaily\",\n            \"offset\": \"PT1H02M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestRBACDaily\",\n            \"offset\": \"PT2H02M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_RecommendationsWeekly\",\n            \"offset\": \"PT2H30M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestRecommendationsWeekly\",\n            \"offset\": \"PT3H30M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestSuppressionsWeekly\",\n            \"offset\": \"PT3H00M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestRemediationLogsDaily\",\n            \"offset\": \"PT1H35M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorVmssCpuMaxHourly\",\n            \"offset\": \"PT1H15M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorVmssCpuAvgHourly\",\n            \"offset\": \"PT1H15M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorVmssMemoryMinHourly\",\n            \"offset\": \"PT1H15M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorSqlDbDtuMaxHourly\",\n            \"offset\": \"PT1H15M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorSqlDbDtuAvgHourly\",\n            \"offset\": \"PT1H16M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorAppServiceCpuMaxHourly\",\n            \"offset\": \"PT1H16M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorAppServiceCpuAvgHourly\",\n            \"offset\": \"PT1H16M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorAppServiceMemoryMaxHourly\",\n            \"offset\": \"PT1H16M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorAppServiceMemoryAvgHourly\",\n            \"offset\": \"PT1H17M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorDiskIOPSHourly\",\n            \"offset\": \"PT1H17M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportMonitorDiskMBPsHourly\",\n            \"offset\": \"PT1H17M\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestAzMonitorMetricsHourly\",\n            \"offset\": \"PT2H\",\n            \"frequency\": \"Hour\"\n        },\n        {\n            \"name\": \"AzureOptimization_CleanUpRecommendationsWeekly\",\n            \"offset\": \"P6D\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportPricesWeekly\",\n            \"offset\": \"PT1H35M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestPricesheetWeekly\",\n            \"offset\": \"PT2H10M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestReservationsPriceWeekly\",\n            \"offset\": \"PT2H10M\",\n            \"frequency\": \"Week\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportReservationsDaily\",\n            \"offset\": \"PT2H\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_ExportSavingsPlansDaily\",\n            \"offset\": \"PT2H05M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestReservationsUsageDaily\",\n            \"offset\": \"PT2H30M\",\n            \"frequency\": \"Day\"\n        },\n        {\n            \"name\": \"AzureOptimization_IngestSavingsPlansUsageDaily\",\n            \"offset\": \"PT2H35M\",\n            \"frequency\": \"Day\"\n        }\n    ],\n    \"baseIngest\": [\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Ingest-OptimizationCSVExportsToLogAnalytics.ps1\",\n                \"version\": \"1.5.0.0\"\n            },\n            \"source\": \"dataCollection\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Ingest-RecommendationsToSQLServer.ps1\",\n                \"version\": \"1.6.5.0\"\n            },\n            \"source\": \"recommendations\",\n            \"schedule\": \"AzureOptimization_IngestRecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Ingest-RecommendationsToLogAnalytics.ps1\",\n                \"version\": \"1.0.2.0\"\n            },\n            \"source\": \"recommendations\",\n            \"schedule\": \"AzureOptimization_IngestRecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Ingest-SuppressionsToLogAnalytics.ps1\",\n                \"version\": \"1.0.0.0\"\n            },\n            \"source\": \"recommendations\",\n            \"schedule\": \"AzureOptimization_IngestSuppressionsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/maintenance/CleanUp-OlderRecommendationsFromSqlServer.ps1\",\n                \"version\": \"1.0.0.0\"\n            },\n            \"source\": \"maintenance\",\n            \"schedule\": \"AzureOptimization_CleanUpRecommendationsWeekly\"\n        }\n    ],\n    \"dataCollection\": [\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-AADObjectsToBlobStorage.ps1\",\n                \"version\": \"1.2.2.1\"\n            },\n            \"container\": \"aadobjectsexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportAADObjectsDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestAADObjectsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGAppGatewayPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.1.4.1\"\n            },\n            \"container\": \"argappgwexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGAppGWsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGAvailabilitySetPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.1.4.1\"\n            },\n            \"container\": \"argavailsetexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGAvailSetsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGLoadBalancerPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.1.4.1\"\n            },\n            \"container\": \"arglbexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGLoadBalancersDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGManagedDisksPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.3.4.1\"\n            },\n            \"container\": \"argdiskexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGDisksDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGNICPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"argnicexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGNICsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGNSGPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"argnsgexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGNSGsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGPublicIpPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"argpublicipexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGPublicIPsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGResourceContainersPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.5.1\"\n            },\n            \"container\": \"argrescontainersexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGResourceContainersDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGUnmanagedDisksPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.1.4.1\"\n            },\n            \"container\": \"argvhdexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGVHDsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGVNetPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"argvnetexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGVNetsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGVirtualMachinesPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.4.4.1\"\n            },\n            \"container\": \"argvmexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGVMsDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGVMSSPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"argvmssexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGVMSSDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGSqlDatabasePropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"argsqldbexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGSqlDbDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ARGAppServicePlanPropertiesToBlobStorage.ps1\",\n                \"version\": \"1.0.1.1\"\n            },\n            \"container\": \"argappserviceplanexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportARGDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestARGAppServicePlanDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-AdvisorRecommendationsToBlobStorage.ps1\",\n                \"version\": \"1.4.2.1\"\n            },\n            \"container\": \"advisorexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportAdvisorWeekly\",\n            \"ingestSchedule\": \"AzureOptimization_IngestAdvisorWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ConsumptionToBlobStorage.ps1\",\n                \"version\": \"2.0.4.1\"\n            },\n            \"container\": \"consumptionexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportConsumptionDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestConsumptionDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-RBACAssignmentsToBlobStorage.ps1\",\n                \"version\": \"1.0.4.1\"\n            },\n            \"container\": \"rbacexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportRBACDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestRBACDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-PolicyComplianceToBlobStorage.ps1\",\n                \"version\": \"1.0.3.1\"\n            },\n            \"container\": \"policystateexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportPolicyStateDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestPolicyStateDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-AzMonitorMetricsToBlobStorage.ps1\",\n                \"version\": \"1.0.2.1\"\n            },\n            \"container\": \"azmonitorexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedules\": [\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorVmssCpuMaxHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.compute/virtualmachinescalesets\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Maximum\",\n                        \"MetricNames\": \"Percentage CPU\",\n                        \"TimeGrain\": \"01:00:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorVmssCpuAvgHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.compute/virtualmachinescalesets\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Average\",\n                        \"MetricNames\": \"Percentage CPU\",\n                        \"TimeGrain\": \"01:00:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorVmssMemoryMinHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.compute/virtualmachinescalesets\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Minimum\",\n                        \"MetricNames\": \"Available Memory Bytes\",\n                        \"TimeGrain\": \"01:00:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorSqlDbDtuMaxHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.sql/servers/databases\",\n                        \"ARGFilter\": \"sku.tier in ('Standard','Premium')\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Maximum\",\n                        \"MetricNames\": \"dtu_consumption_percent\",\n                        \"TimeGrain\": \"01:00:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorSqlDbDtuAvgHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.sql/servers/databases\",\n                        \"ARGFilter\": \"sku.tier in ('Standard','Premium')\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"AggregationOfType\": \"Maximum\",\n                        \"aggregationType\": \"Average\",\n                        \"MetricNames\": \"dtu_consumption_percent\",\n                        \"TimeGrain\": \"00:01:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorAppServiceCpuMaxHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.web/serverfarms\",\n                        \"ARGFilter\": \"properties.computeMode == 'Dedicated' and sku.tier != 'Free'\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Maximum\",\n                        \"MetricNames\": \"CpuPercentage\",\n                        \"TimeGrain\": \"01:00:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorAppServiceCpuAvgHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.web/serverfarms\",\n                        \"ARGFilter\": \"properties.computeMode == 'Dedicated' and sku.tier != 'Free'\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Average\",\n                        \"AggregationOfType\": \"Maximum\",\n                        \"MetricNames\": \"CpuPercentage\",\n                        \"TimeGrain\": \"00:01:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorAppServiceMemoryMaxHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.web/serverfarms\",\n                        \"ARGFilter\": \"properties.computeMode == 'Dedicated' and sku.tier != 'Free'\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Maximum\",\n                        \"MetricNames\": \"MemoryPercentage\",\n                        \"TimeGrain\": \"01:00:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorAppServiceMemoryAvgHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.web/serverfarms\",\n                        \"ARGFilter\": \"properties.computeMode == 'Dedicated' and sku.tier != 'Free'\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Average\",\n                        \"AggregationOfType\": \"Maximum\",\n                        \"MetricNames\": \"MemoryPercentage\",\n                        \"TimeGrain\": \"00:01:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorDiskIOPSHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.compute/disks\",\n                        \"ARGFilter\": \"sku.name =~ 'Premium_LRS' and properties.diskState != 'Unattached'\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Average\",\n                        \"AggregationOfType\": \"Maximum\",\n                        \"MetricNames\": \"Composite Disk Read Operations/sec,Composite Disk Write Operations/sec\",\n                        \"TimeGrain\": \"00:01:00\"\n                    }\n                },\n                {\n                    \"schedule\": \"AzureOptimization_ExportMonitorDiskMBPsHourly\",\n                    \"parameters\": {\n                        \"ResourceType\": \"microsoft.compute/disks\",\n                        \"ARGFilter\": \"sku.name =~ 'Premium_LRS' and properties.diskState != 'Unattached'\",\n                        \"TimeSpan\": \"01:00:00\",\n                        \"aggregationType\": \"Average\",\n                        \"AggregationOfType\": \"Maximum\",\n                        \"MetricNames\": \"Composite Disk Read Bytes/sec,Composite Disk Write Bytes/sec\",\n                        \"TimeGrain\": \"00:01:00\"\n                    }\n                }\n            ],\n            \"ingestSchedule\": \"AzureOptimization_IngestAzMonitorMetricsHourly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-PriceSheetToBlobStorage.ps1\",\n                \"version\": \"1.1.1.1\"\n            },\n            \"container\": \"pricesheetexports\",\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_PriceSheetMeterCategories\",\n                    \"defaultValue\": \"Virtual Machines,Storage\"\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportPricesWeekly\",\n            \"ingestSchedule\": \"AzureOptimization_IngestPricesheetWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ReservationsPriceToBlobStorage.ps1\",\n                \"version\": \"1.0.1.1\"\n            },\n            \"container\": \"reservationspriceexports\",\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RetailPricesCurrencyCode\",\n                    \"defaultValue\": \"EUR\"\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportPricesWeekly\",\n            \"ingestSchedule\": \"AzureOptimization_IngestReservationsPriceWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-ReservationsUsageToBlobStorage.ps1\",\n                \"version\": \"1.1.2.1\"\n            },\n            \"container\": \"reservationsexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportReservationsDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestReservationsUsageDaily\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/data-collection/Export-SavingsPlansUsageToBlobStorage.ps1\",\n                \"version\": \"1.0.0.0\"\n            },\n            \"container\": \"savingsplansexports\",\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_ExportSavingsPlansDaily\",\n            \"ingestSchedule\": \"AzureOptimization_IngestSavingsPlansUsageDaily\"\n        }\n    ],\n    \"recommendations\": [\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-AADExpiringCredentialsToBlobStorage.ps1\",\n                \"version\": \"1.1.10.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RecommendationAADMinCredValidityDays\",\n                    \"defaultValue\": 30\n                },\n                {\n                    \"name\": \"AzureOptimization_RecommendationAADMaxCredValidityYears\",\n                    \"defaultValue\": 2\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-AdvisorAsIsToBlobStorage.ps1\",\n                \"version\": \"1.5.5.0\"\n            },\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-AdvisorCostAugmentedToBlobStorage.ps1\",\n                \"version\": \"2.9.1.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_ConsumptionOffsetDays\",\n                    \"defaultValue\": 3\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-VMOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.0.0.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RecommendationLongDeallocatedVmsIntervalDays\",\n                    \"defaultValue\": 30\n                },\n                {\n                    \"name\": \"AzureOptimization_ConsumptionOffsetDays\",\n                    \"defaultValue\": 3\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-UnattachedDisksToBlobStorage.ps1\",\n                \"version\": \"2.4.8.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_ConsumptionOffsetDays\",\n                    \"defaultValue\": 3\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-UnusedAppGWsToBlobStorage.ps1\",\n                \"version\": \"1.2.9.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_ConsumptionOffsetDays\",\n                    \"defaultValue\": 3\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-UnusedLoadBalancersToBlobStorage.ps1\",\n                \"version\": \"1.2.9.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_ConsumptionOffsetDays\",\n                    \"defaultValue\": 3\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-VMsHighAvailabilityToBlobStorage.ps1\",\n                \"version\": \"1.0.3.0\"\n            },\n            \"requiredVariables\": [\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-ARMOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.0.3.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RecommendationRBACAssignmentsPercentageThreshold\",\n                    \"defaultValue\": 80\n                },\n                {\n                    \"name\": \"AzureOptimization_RecommendationResourceGroupsPerSubPercentageThreshold\",\n                    \"defaultValue\": 80\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-VNetOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.0.4.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RecommendationVNetSubnetMaxUsedPercentageThreshold\",\n                    \"defaultValue\": 80\n                },\n                {\n                    \"name\": \"AzureOptimization_RecommendationVNetSubnetMinUsedPercentageThreshold\",\n                    \"defaultValue\": 5\n                },\n                {\n                    \"name\": \"AzureOptimization_RecommendationVNetSubnetEmptyMinAgeInDays\",\n                    \"defaultValue\": 30\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-VMSSOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.1.1.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdCpuDegradedMaxPercentage\",\n                    \"defaultValue\": 95\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdCpuDegradedAvgPercentage\",\n                    \"defaultValue\": 75\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdMemoryDegradedPercentage\",\n                    \"defaultValue\": 90\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-SqlDbOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.1.2.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_PerfPercentileSqlDtu\",\n                    \"defaultValue\": 99\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdDtuPercentage\",\n                    \"defaultValue\": 40\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdDtuDegradedPercentage\",\n                    \"defaultValue\": 75\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-StorageAccountOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.0.3.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RecommendationStorageAcountGrowthThresholdPercentage\",\n                    \"defaultValue\": 5\n                },\n                {\n                    \"name\": \"AzureOptimization_RecommendationStorageAcountGrowthMonthlyCostThreshold\",\n                    \"defaultValue\": 50\n                },\n                {\n                    \"name\": \"AzureOptimization_RecommendationStorageAcountGrowthLookbackDays\",\n                    \"defaultValue\": 30\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-AppServiceOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.0.3.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdCpuDegradedMaxPercentage\",\n                    \"defaultValue\": 95\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdCpuDegradedAvgPercentage\",\n                    \"defaultValue\": 75\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdMemoryDegradedPercentage\",\n                    \"defaultValue\": 90\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/recommendations/Recommend-DiskOptimizationsToBlobStorage.ps1\",\n                \"version\": \"1.1.1.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdDiskIOPSPercentage\",\n                    \"defaultValue\": 5\n                },\n                {\n                    \"name\": \"AzureOptimization_PerfThresholdDiskMBsPercentage\",\n                    \"defaultValue\": 5\n                }\n            ],\n            \"exportSchedule\": \"AzureOptimization_RecommendationsWeekly\"\n        }\n    ],\n    \"remediations\": [\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/remediations/Remediate-AdvisorRightSizeFiltered.ps1\",\n                \"version\": \"1.2.4.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RemediateRightSizeMinFitScore\",\n                    \"defaultValue\": 5.0\n                },\n                {\n                    \"name\": \"AzureOptimization_RemediateRightSizeMinWeeksInARow\",\n                    \"defaultValue\": 4\n                }\n            ]\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/remediations/Remediate-LongDeallocatedVMsFiltered.ps1\",\n                \"version\": \"1.0.3.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RemediateLongDeallocatedVMsMinFitScore\",\n                    \"defaultValue\": 5.0\n                },\n                {\n                    \"name\": \"AzureOptimization_RemediateLongDeallocatedVMsMinWeeksInARow\",\n                    \"defaultValue\": 4\n                }\n            ]\n        },\n        {\n            \"runbook\": {\n                \"name\": \"runbooks/remediations/Remediate-UnattachedDisksFiltered.ps1\",\n                \"version\": \"1.0.3.0\"\n            },\n            \"requiredVariables\": [\n                {\n                    \"name\": \"AzureOptimization_RemediateUnattachedDisksMinFitScore\",\n                    \"defaultValue\": 5.0\n                },\n                {\n                    \"name\": \"AzureOptimization_RemediateUnattachedDisksMinWeeksInARow\",\n                    \"defaultValue\": 4\n                },\n                {\n                    \"name\": \"AzureOptimization_RemediateUnattachedDisksAction\",\n                    \"defaultValue\": \"Delete\"\n                }\n            ]\n        }\n    ],\n    \"deprecatedRunbooks\": [\n        \"Recommend-AvailSetsWithLowFaultDomainCountToBlobStorage\",\n        \"Recommend-AvailSetsWithLowUpdateDomainCountToBlobStorage\",\n        \"Recommend-AvailSetsWithVMsSharingStorageAccountsToBlobStorage\",\n        \"Recommend-StorageAccountsWithMultipleVMsToBlobStorage\",\n        \"Recommend-VMsNoAvailSetToBlobStorage\",\n        \"Recommend-VMsSingleInAvailSetToBlobStorage\",\n        \"Recommend-VMsWithDisksMultipleStorageAccountsToBlobStorage\",\n        \"Recommend-VMsWithUnmanagedDisksToBlobStorage\",\n        \"Recommend-LongDeallocatedVmsToBlobStorage\"\n    ],\n    \"overwriteVariables\": [\n        {\n            \"name\": \"AzureOptimization_LogAnalyticsChunkSize\",\n            \"value\": 6000\n        }\n    ]\n}"
  },
  {
    "path": "views/powerbi-query.m",
    "content": "let\n    Source = Sql.Database(\"aoedevgithub-sql.database.windows.net\", \"azureoptimization\", [Query=\"EXEC GetRecommendations\", CommandTimeout=#duration(0, 2, 0, 0)]),\n    #\"Parsed JSON Tags\" = Table.TransformColumns(Source,{{\"Tags\", Json.Document}}),\n    #\"Expanded Tags\" = Table.ExpandRecordColumn(#\"Parsed JSON Tags\", \"Tags\", {\"environment\", \"costcenter\"}, {\"Tags.Environment\", \"Tags.CostCenter\"}),\n    #\"Trimmed Text\" = Table.TransformColumns(#\"Expanded Tags\",{{\"Tags.Environment\", Text.Trim, type text}, {\"Tags.CostCenter\", Text.Trim, type text}}),\n    #\"Duplicated AdditionalInfo\" = Table.DuplicateColumn(#\"Trimmed Text\",\"AdditionalInfo\", \"AddInfoJSON\"),\n    #\"Parsed JSON AdditionalInfo\" = Table.TransformColumns(#\"Duplicated AdditionalInfo\",{{\"AdditionalInfo\", Json.Document}}),\n    #\"Expanded AdditionalInfo\" = Table.ExpandRecordColumn(#\"Parsed JSON AdditionalInfo\", \"AdditionalInfo\", {\"BelowNetworkThreshold\", \"SupportsDataDisksCount\", \"MetricIOPS\", \"SupportsIOPS\", \"BelowMemoryThreshold\", \"currentSku\", \"targetSku\", \"SupportsNICCount\", \"DataDiskCount\", \"MetricMemoryPercentage\", \"MetricNetworkMbps\", \"MetricMiBps\", \"SupportsMiBps\", \"BelowCPUThreshold\", \"NicCount\", \"MetricCPUPercentage\", \"annualSavingsAmount\", \"savingsCurrency\", \"savingsAmount\", \"CostsAmount\", \"reservationType\", \"vmSize\", \"DiskType\", \"DiskSizeGB\", \"MaxMemoryP95\", \"MaxCpuP95\", \"MaxTotalNetworkP95\", \"DeploymentModel\", \"scope\", \"region\", \"qty\", \"displaySKU\", \"term\"}, {\"AddInfo.BelowNetworkThreshold\", \"AddInfo.SupportsDataDisksCount\", \"AddInfo.MetricIOPS\", \"AddInfo.SupportsIOPS\", \"AddInfo.BelowMemoryThreshold\", \"AddInfo.currentSku\", \"AddInfo.targetSku\", \"AddInfo.SupportsNICCount\", \"AddInfo.DataDiskCount\", \"AddInfo.MetricMemoryPercentage\", \"AddInfo.MetricNetworkMbps\", \"AddInfo.MetricMiBps\", \"AddInfo.SupportsMiBps\", \"AddInfo.BelowCPUThreshold\", \"AddInfo.NicCount\", \"AddInfo.MetricCPUPercentage\", \"AddInfo.annualSavingsAmount\", \"AddInfo.savingsCurrency\", \"AddInfo.savingsAmount\", \"AddInfo.CostsAmount\", \"AddInfo.reservationType\", \"AddInfo.vmSize\", \"AddInfo.DiskType\", \"AddInfo.DiskSizeGB\", \"AddInfo.MaxMemoryP95\", \"AddInfo.MaxCpuP95\", \"AddInfo.MaxTotalNetworkP95\", \"AddInfo.DeploymentModel\", \"AddInfo.Scope\", \"AddInfo.Region\", \"AddInfo.Quantity\", \"AddInfo.ReservationSKU\", \"AddInfo.Term\"}),\n    #\"Split Column by Delimiter\" = Table.SplitColumn(#\"Expanded AdditionalInfo\", \"AddInfo.BelowNetworkThreshold\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.BelowNetworkThreshold.1\", \"AddInfo.BelowNetworkThreshold.2\"}),\n    #\"Changed Type\" = Table.TransformColumnTypes(#\"Split Column by Delimiter\",{{\"AddInfo.BelowNetworkThreshold.1\", type text}, {\"AddInfo.BelowNetworkThreshold.2\", type text}}),\n    #\"Renamed Columns\" = Table.RenameColumns(#\"Changed Type\",{{\"AddInfo.BelowNetworkThreshold.2\", \"AddInfo.BelowNetworkThresholdDetails\"}, {\"AddInfo.BelowNetworkThreshold.1\", \"AddInfo.BelowNetworkThresholdResult\"}}),\n    #\"Split Column by Delimiter1\" = Table.SplitColumn(#\"Renamed Columns\", \"AddInfo.SupportsDataDisksCount\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.SupportsDataDisksCount.1\", \"AddInfo.SupportsDataDisksCount.2\"}),\n    #\"Changed Type1\" = Table.TransformColumnTypes(#\"Split Column by Delimiter1\",{{\"AddInfo.SupportsDataDisksCount.1\", type logical}, {\"AddInfo.SupportsDataDisksCount.2\", type text}}),\n    #\"Renamed Columns1\" = Table.RenameColumns(#\"Changed Type1\",{{\"AddInfo.SupportsDataDisksCount.1\", \"AddInfo.SupportsDataDisksCountResult\"}, {\"AddInfo.SupportsDataDisksCount.2\", \"AddInfo.SupportsDataDisksCountDetails\"}}),\n    #\"Split Column by Delimiter2\" = Table.SplitColumn(#\"Renamed Columns1\", \"AddInfo.SupportsIOPS\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.SupportsIOPS.1\", \"AddInfo.SupportsIOPS.2\"}),\n    #\"Changed Type2\" = Table.TransformColumnTypes(#\"Split Column by Delimiter2\",{{\"AddInfo.SupportsIOPS.1\", type text}, {\"AddInfo.SupportsIOPS.2\", type text}}),\n    #\"Renamed Columns2\" = Table.RenameColumns(#\"Changed Type2\",{{\"AddInfo.SupportsIOPS.1\", \"AddInfo.SupportsIOPSResult\"}, {\"AddInfo.SupportsIOPS.2\", \"AddInfo.SupportsIOPSDetails\"}}),\n    #\"Split Column by Delimiter3\" = Table.SplitColumn(#\"Renamed Columns2\", \"AddInfo.BelowMemoryThreshold\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.BelowMemoryThreshold.1\", \"AddInfo.BelowMemoryThreshold.2\"}),\n    #\"Changed Type3\" = Table.TransformColumnTypes(#\"Split Column by Delimiter3\",{{\"AddInfo.BelowMemoryThreshold.1\", type text}, {\"AddInfo.BelowMemoryThreshold.2\", type text}}),\n    #\"Renamed Columns3\" = Table.RenameColumns(#\"Changed Type3\",{{\"AddInfo.BelowMemoryThreshold.1\", \"AddInfo.BelowMemoryThresholdResult\"}, {\"AddInfo.BelowMemoryThreshold.2\", \"AddInfo.BelowMemoryThresholdDetails\"}}),\n    #\"Split Column by Delimiter4\" = Table.SplitColumn(#\"Renamed Columns3\", \"AddInfo.SupportsNICCount\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.SupportsNICCount.1\", \"AddInfo.SupportsNICCount.2\"}),\n    #\"Changed Type4\" = Table.TransformColumnTypes(#\"Split Column by Delimiter4\",{{\"AddInfo.SupportsNICCount.1\", type logical}, {\"AddInfo.SupportsNICCount.2\", type text}}),\n    #\"Renamed Columns4\" = Table.RenameColumns(#\"Changed Type4\",{{\"AddInfo.SupportsNICCount.1\", \"AddInfo.SupportsNICCountResult\"}, {\"AddInfo.SupportsNICCount.2\", \"AddInfo.SupportsNICCountDetails\"}}),\n    #\"Split Column by Delimiter5\" = Table.SplitColumn(#\"Renamed Columns4\", \"AddInfo.SupportsMiBps\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.SupportsMiBps.1\", \"AddInfo.SupportsMiBps.2\"}),\n    #\"Changed Type5\" = Table.TransformColumnTypes(#\"Split Column by Delimiter5\",{{\"AddInfo.SupportsMiBps.1\", type text}, {\"AddInfo.SupportsMiBps.2\", type text}}),\n    #\"Renamed Columns5\" = Table.RenameColumns(#\"Changed Type5\",{{\"AddInfo.SupportsMiBps.1\", \"AddInfo.SupportsMiBpsResult\"}, {\"AddInfo.SupportsMiBps.2\", \"AddInfo.SupportsMiBpsDetails\"}}),\n    #\"Split Column by Delimiter6\" = Table.SplitColumn(#\"Renamed Columns5\", \"AddInfo.BelowCPUThreshold\", Splitter.SplitTextByDelimiter(\":\", QuoteStyle.None), {\"AddInfo.BelowCPUThreshold.1\", \"AddInfo.BelowCPUThreshold.2\"}),\n    #\"Changed Type6\" = Table.TransformColumnTypes(#\"Split Column by Delimiter6\",{{\"AddInfo.BelowCPUThreshold.1\", type text}, {\"AddInfo.BelowCPUThreshold.2\", type text}}),\n    #\"Renamed Columns6\" = Table.RenameColumns(#\"Changed Type6\",{{\"AddInfo.BelowCPUThreshold.1\", \"AddInfo.BelowCPUThresholdResult\"}, {\"AddInfo.BelowCPUThreshold.2\", \"AddInfo.BelowCPUThresholdDetails\"}}),\n    #\"Changed Type7\" = Table.TransformColumnTypes(#\"Renamed Columns6\",{{\"AddInfo.CostsAmount\", type number}}, \"en-US\"),\n\t#\"Changed Type8\" = Table.TransformColumnTypes(#\"Changed Type7\",{{\"AddInfo.savingsAmount\", type number}}, \"en-US\"),\n\t#\"Changed Type9\" = Table.TransformColumnTypes(#\"Changed Type8\",{{\"AddInfo.DiskSizeGB\", Int64.Type}}),\n\t#\"Changed Type10\" = Table.TransformColumnTypes(#\"Changed Type9\",{{\"AddInfo.MaxMemoryP95\", Int64.Type}}),\n\t#\"Changed Type11\" = Table.TransformColumnTypes(#\"Changed Type10\",{{\"AddInfo.MaxCpuP95\", Int64.Type}}),\n\t#\"Changed Type12\" = Table.TransformColumnTypes(#\"Changed Type11\",{{\"AddInfo.MaxTotalNetworkP95\", Int64.Type}}),\n\t#\"Changed Type13\" = Table.TransformColumnTypes(#\"Changed Type12\",{{\"AddInfo.annualSavingsAmount\", type number}}, \"en-US\"),\n\t#\"Changed Type14\" = Table.TransformColumnTypes(#\"Changed Type13\",{{\"AddInfo.MetricCPUPercentage\", type number}}, \"en-US\"),\n\t#\"Changed Type15\" = Table.TransformColumnTypes(#\"Changed Type14\",{{\"AddInfo.NicCount\", Int64.Type}}),\n\t#\"Changed Type16\" = Table.TransformColumnTypes(#\"Changed Type15\",{{\"AddInfo.DataDiskCount\", Int64.Type}}),\n\t#\"Changed Type17\" = Table.TransformColumnTypes(#\"Changed Type16\",{{\"AddInfo.MetricMiBps\", type number}}, \"en-US\"),\n\t#\"Changed Type18\" = Table.TransformColumnTypes(#\"Changed Type17\",{{\"AddInfo.MetricIOPS\", type number}}, \"en-US\"),\n\t#\"Changed Type19\" = Table.TransformColumnTypes(#\"Changed Type18\",{{\"AddInfo.MetricNetworkMbps\", type number}}, \"en-US\"),\n\t#\"Changed Type20\" = Table.TransformColumnTypes(#\"Changed Type19\",{{\"AddInfo.MetricMemoryPercentage\", type number}}, \"en-US\")\nin\n    #\"Changed Type20\""
  },
  {
    "path": "views/workbooks/benefits-simulation.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Benefits Simulation'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '96fabefe-1f3e-4526-a5db-c442661617e5'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('benefits-simulation.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/benefits-simulation.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"b58b4eb8-5821-44d2-bc7e-54054df27320\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LookbackPeriod\",\n            \"label\": \"Lookback Period\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"value\": {\n              \"durationMs\": 2592000000\n            }\n          },\n          {\n            \"id\": \"121c03f2-6ca3-4438-a828-8dffec3e208a\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Subscriptions\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| distinct SubscriptionName\\r\\n| order by SubscriptionName asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ]\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"0c9fd864-c30b-4f8f-8065-e44aa1b55a51\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"GroupBy\",\n            \"label\": \"Group by\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[{ \\\"value\\\": \\\"SubscriptionName\\\", \\\"label\\\": \\\"Subscription\\\", \\\"selected\\\": true }, { \\\"value\\\": \\\"SKUName\\\", \\\"label\\\": \\\"VM Size\\\" },{ \\\"value\\\": \\\"ISFGroup\\\", \\\"label\\\": \\\"Instance Size Flexibility Group\\\" }]\"\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters - 0\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Unless specified, usage values unit corresponds to your billing currency. **Only applies to Virtual Machines usage in Azure Global**.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"text - 7\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"If below tabs are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).\",\n        \"style\": \"warning\"\n      },\n      \"name\": \"text - 6\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s)['ServiceType'])\\r\\n| extend SKUName = strcat(VMSize, ' ', ResourceLocation_s)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice=todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s)), SubscriptionName, VMSize, SKUName\\r\\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| extend ISFGroup = strcat(ISFGroup, ' ', ResourceLocation_s)\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| summarize HourlyCost=sum(OnDemandCost)/24 by bin(todatetime(Date_s), 1d), {GroupBy}\",\n        \"size\": 1,\n        \"aggregation\": 3,\n        \"title\": \"Average On-Demand hourly usage (actual cost)\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"barchart\",\n        \"chartSettings\": {\n          \"seriesLabelSettings\": [\n            {\n              \"seriesName\": \"Reservation\",\n              \"color\": \"green\"\n            },\n            {\n              \"seriesName\": \"OnDemand\",\n              \"color\": \"red\"\n            }\n          ]\n        }\n      },\n      \"name\": \"onDemandUsageAsIs\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"1d33e1ca-6d19-4b74-8903-00c5671b8f87\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Savings Plans\",\n            \"subTarget\": \"SavingsPlans\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"b26e72a1-167d-4449-a8b6-6665814331a3\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Reservations\",\n            \"subTarget\": \"Reservations\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"analysisTabs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"Enter a hourly commitment to estimate your savings according to the lookback period\",\n              \"style\": \"info\"\n            },\n            \"name\": \"text - 1\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"977209de-fdec-4c84-86fd-7b0815aa71e1\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"SavingsPlanTerm\",\n                  \"label\": \"Savings Plan Term\",\n                  \"type\": 10,\n                  \"description\": \"Savings Plan term to get the Savings Plan prices from\",\n                  \"isRequired\": true,\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"jsonData\": \"[\\\"1 Year\\\", \\\"3 Years\\\"]\",\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"3 Years\"\n                },\n                {\n                  \"id\": \"368dffea-79c4-45a9-9876-fc77cf691541\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"HourlyCommitment\",\n                  \"label\": \"Hourly commitment\",\n                  \"type\": 1,\n                  \"isRequired\": true,\n                  \"value\": \"\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"savingsPlansParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nlet LinuxSavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and MeterSubCategory_s !endswith \\\"Windows\\\" and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet SavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxSavingsPlanPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend SavingsPlanPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, SavingsPlanPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s)['ServiceType'])\\r\\n| extend ResourceLocation_s=tolower(ResourceLocation_s)\\r\\n| extend SKUName = strcat(VMSize, ' ', ResourceLocation_s)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice, SubscriptionName, VMSize, SKUName\\r\\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| extend ISFGroup = strcat(ISFGroup, ' ', ResourceLocation_s)\\r\\n| join kind=inner ( SavingsPlanPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| extend SavingsPlanCost = QtyHours * SavingsPlanPrice\\r\\n| summarize HourlyCost=sum(SavingsPlanCost)/24 by todatetime(Date_s), {GroupBy}\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Average On-Demand hourly usage (Savings Plan prices)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageSavingsPlansPerspective\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let HourlyCommitment = {HourlyCommitment};\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nlet LinuxSavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and MeterSubCategory_s !endswith \\\"Windows\\\" and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet SavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxSavingsPlanPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend SavingsPlanPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, SavingsPlanPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend ResourceLocation_s=tolower(ResourceLocation_s)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice, SubscriptionName\\r\\n| join kind=inner ( SavingsPlanPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| extend SavingsPlanCost = QtyHours * SavingsPlanPrice\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| extend SavingsPlanDiscount = 1 - SavingsPlanPrice / OnDemandPrice\\r\\n| extend MeterAndRegion = strcat(BillingMeter, ' - ', ArmRegion)\\r\\n| summarize HourlySavingsPlanCost=sum(SavingsPlanCost)/24, HourlyOnDemandCost=sum(OnDemandCost)/24, SavingsPlanDiscount=round(avg(SavingsPlanDiscount), 3) by Date_s, MeterAndRegion\\r\\n| order by todatetime(Date_s), SavingsPlanDiscount\\r\\n| extend SavingsPlanHourlyBalance = row_cumsum(HourlySavingsPlanCost, prev(Date_s) != Date_s)\\r\\n| extend CutOff = iif(prev(Date_s) != Date_s or prev(SavingsPlanHourlyBalance, 1, 0) < HourlyCommitment, false, true)\\r\\n| where not(CutOff)\\r\\n| extend SavingsAllocationPercentage = iif(SavingsPlanHourlyBalance < HourlyCommitment, 1.0, 1 - (SavingsPlanHourlyBalance - HourlyCommitment) / HourlySavingsPlanCost)\\r\\n| summarize SavedAmount=sum((HourlyOnDemandCost - HourlySavingsPlanCost) * SavingsAllocationPercentage)*24 by todatetime(Date_s)\",\n              \"size\": 1,\n              \"title\": \"Estimated savings (in your billing currency)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavedAmount\",\n                    \"label\": \"Saved Amount\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageSavingsPlansSimulation\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let HourlyCommitment = {HourlyCommitment};\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nlet LinuxSavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and MeterSubCategory_s !endswith \\\"Windows\\\" and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet SavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxSavingsPlanPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend SavingsPlanPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, SavingsPlanPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend ResourceLocation_s=tolower(ResourceLocation_s)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice, SubscriptionName\\r\\n| join kind=inner ( SavingsPlanPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| extend SavingsPlanCost = QtyHours * SavingsPlanPrice\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| extend SavingsPlanDiscount = 1 - SavingsPlanPrice / OnDemandPrice\\r\\n| extend MeterAndRegion = strcat(BillingMeter, ' - ', ArmRegion)\\r\\n| summarize HourlySavingsPlanCost=sum(SavingsPlanCost)/24, HourlyOnDemandCost=sum(OnDemandCost)/24, SavingsPlanDiscount=round(avg(SavingsPlanDiscount), 3) by Date_s, MeterAndRegion\\r\\n| order by todatetime(Date_s), SavingsPlanDiscount\\r\\n| extend SavingsPlanHourlyBalance = row_cumsum(HourlySavingsPlanCost, prev(Date_s) != Date_s)\\r\\n| extend CutOff = iif(prev(Date_s) != Date_s or prev(SavingsPlanHourlyBalance, 1, 0) < HourlyCommitment, false, true)\\r\\n| where not(CutOff)\\r\\n| extend SavingsAllocationPercentage = iif(SavingsPlanHourlyBalance < HourlyCommitment, 1.0, 1 - (SavingsPlanHourlyBalance - HourlyCommitment) / HourlySavingsPlanCost)\\r\\n| summarize SavedAmount=sum((HourlyOnDemandCost - HourlySavingsPlanCost) * SavingsAllocationPercentage)*24, AmountWouldSpendOnDemand=(sum(HourlyOnDemandCost * SavingsAllocationPercentage))*24 by Date_s\\r\\n| extend SavedPercentage = SavedAmount / AmountWouldSpendOnDemand\\r\\n| project SavedPercentage, todatetime(Date_s)\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Estimated savings (percentage)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlanUsagePercentage\",\n                    \"label\": \"Efficiency\"\n                  }\n                ],\n                \"ySettings\": {\n                  \"numberFormatSettings\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"percent\",\n                      \"useGrouping\": true\n                    }\n                  }\n                }\n              }\n            },\n            \"name\": \"onDemandUsageSavingsPlansPercentageSimulation\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let HourlyCommitment = {HourlyCommitment};\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nlet LinuxSavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and MeterSubCategory_s !endswith \\\"Windows\\\" and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet SavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxSavingsPlanPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend SavingsPlanPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, SavingsPlanPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend ResourceLocation_s=tolower(ResourceLocation_s)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice=OnDemandPrice, SubscriptionName\\r\\n| join kind=inner ( SavingsPlanPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| extend SavingsPlanCost = QtyHours * SavingsPlanPrice\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| extend SavingsPlanDiscount = 1 - SavingsPlanPrice / OnDemandPrice\\r\\n| extend MeterAndRegion = strcat(BillingMeter, ' - ', ArmRegion)\\r\\n| summarize HourlySavingsPlanCost=sum(SavingsPlanCost)/24, HourlyOnDemandCost=sum(OnDemandCost)/24, SavingsPlanDiscount=round(avg(SavingsPlanDiscount), 3) by Date_s, MeterAndRegion\\r\\n| order by todatetime(Date_s), SavingsPlanDiscount\\r\\n| extend SavingsPlanHourlyBalance = row_cumsum(HourlySavingsPlanCost, prev(Date_s) != Date_s)\\r\\n| extend CutOff = iif(prev(Date_s) != Date_s or prev(SavingsPlanHourlyBalance, 1, 0) < HourlyCommitment, false, true)\\r\\n| where not(CutOff)\\r\\n| extend SavingsAllocationPercentage = iif(SavingsPlanHourlyBalance < HourlyCommitment, 1.0, 1 - (SavingsPlanHourlyBalance - HourlyCommitment) / HourlySavingsPlanCost)\\r\\n| summarize AmountSpentOnSavingsPlan=(sum(HourlySavingsPlanCost * SavingsAllocationPercentage))*24 by todatetime(Date_s)\\r\\n| extend SavingsPlanUsagePercentage = AmountSpentOnSavingsPlan / ({HourlyCommitment}*24)\\r\\n| project-away AmountSpentOnSavingsPlan\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Estimated efficiency\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlanUsagePercentage\",\n                    \"label\": \"Savings Plan Usage\"\n                  }\n                ],\n                \"customThresholdLine\": \"1\",\n                \"customThresholdLineStyle\": 4,\n                \"ySettings\": {\n                  \"numberFormatSettings\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"percent\",\n                      \"useGrouping\": true\n                    }\n                  }\n                }\n              }\n            },\n            \"name\": \"onDemandUsageSavingsPlansEfficiencySimulation\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"SavingsPlans\"\n      },\n      \"name\": \"savingsPlansGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"Enter the number of reserved instances for a given size and region to estimate your savings according to the lookback period.\",\n              \"style\": \"info\"\n            },\n            \"name\": \"text - 5\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"5955658a-2b0d-4e33-b68d-a55f0792bd48\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"ReservationTerm\",\n                  \"label\": \"Reservation Term\",\n                  \"type\": 10,\n                  \"description\": \"Reservation term to get the Reservations prices from\",\n                  \"isRequired\": true,\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": []\n                  },\n                  \"jsonData\": \"[\\\"1 Year\\\", \\\"3 Years\\\"]\",\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"3 Years\"\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"UseISF\",\n                  \"label\": \"Instance Size Flexibility?\",\n                  \"type\": 10,\n                  \"isRequired\": true,\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"jsonData\": \"[\\\"Yes\\\", \\\"No\\\"]\",\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"No\",\n                  \"id\": \"a3a21a9b-d89d-4d30-a3fd-0ec6a1896602\"\n                },\n                {\n                  \"id\": \"8b97a186-0656-4edc-bc7f-d2077ba9ade0\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"VMSize\",\n                  \"label\": \"Size\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend ServiceType = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( ISFGroups ) on $left.ServiceType == $right.ArmSKUName\\r\\n| distinct ServiceType\\r\\n| order by ServiceType asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 0\n                  },\n                  \"timeContextFromParameter\": \"LookbackPeriod\",\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": null\n                },\n                {\n                  \"id\": \"52beb8df-e32a-4ce1-b38e-c363c2a4714f\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"VMRegion\",\n                  \"label\": \"Region\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend ServiceType = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( ISFGroups ) on $left.ServiceType == $right.ArmSKUName\\r\\n| where ServiceType == '{VMSize}'\\r\\n| extend ResourceLocation_s=tolower(ResourceLocation_s)\\r\\n| distinct ResourceLocation_s\\r\\n| order by ResourceLocation_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 2592000000\n                  },\n                  \"timeContextFromParameter\": \"LookbackPeriod\",\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": null\n                },\n                {\n                  \"id\": \"fa4fe488-5590-4df7-8a22-a3f9639892e6\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"VMQuantity\",\n                  \"label\": \"Quantity\",\n                  \"type\": 1,\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 0\n                  },\n                  \"timeContextFromParameter\": \"LookbackPeriod\",\n                  \"value\": \"\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"reservationsParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = materialize(externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName));\\r\\nlet SelectedISFGroup = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project ISFGroup);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend ServiceType = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( ISFGroups ) on $left.ServiceType == $right.ArmSKUName\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), ArmRegion=tolower(ResourceLocation_s), MeterId=MeterId_g, ResourceId, ServiceType, SubscriptionName, ISFGroup\\r\\n| where iif('{UseISF}' == 'Yes', ISFGroup == SelectedISFGroup, ServiceType == '{VMSize}')\\r\\n| where ArmRegion == '{VMRegion}'\\r\\n| extend ReservationSKU = strcat(ServiceType, ' ', ArmRegion)\\r\\n| summarize HourlyVMs=sum(QtyHours/24) by bin(todatetime(Date_s), 1d), ReservationSKU\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Average On-Demand usage (VMs #)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageReservationsPerspective\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = materialize(externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName));\\r\\nlet SelectedISFGroup = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project ISFGroup);\\r\\nlet ISFRatio = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project Ratio);\\r\\nlet TermDivider = iif('{ReservationTerm}' == '3 Years', 3, 1);\\r\\nlet VMQuantity = {VMQuantity};\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend ServiceType = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), ArmRegion=tolower(ResourceLocation_s), ServiceType, MeterId=MeterId_g, ResourceId, OnDemandPrice, SubscriptionName\\r\\n| join kind=inner ( ISFGroups ) on $left.ServiceType == $right.ArmSKUName\\r\\n| where iif('{UseISF}' == 'Yes', ISFGroup == SelectedISFGroup, ServiceType == '{VMSize}')\\r\\n| where ArmRegion == '{VMRegion}'\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationReservationsPriceV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where serviceName_s == 'Virtual Machines' and reservationTerm_s == '{ReservationTerm}'\\r\\n    | extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\\r\\n    | summarize arg_max(TimeGenerated, ReservationPrice) by ArmRegion=tolower(armRegionName_s), ServiceType=tolower(armSkuName_s)\\r\\n) on ArmRegion and ServiceType\\r\\n| order by todatetime(Date_s)\\r\\n| extend RIConsumed = row_cumsum(QtyHours * Ratio / ISFRatio / 24, prev(Date_s) != Date_s)\\r\\n| extend CutOff = iif(prev(Date_s) != Date_s or prev(RIConsumed, 1, 0) < VMQuantity, false, true)\\r\\n| where not(CutOff)\\r\\n| extend RICost = QtyHours * ReservationPrice\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| summarize DailyRICost=sum(RICost), DailyOnDemandCost=sum(OnDemandCost), MaxRIConsumed=max(RIConsumed) by Date_s\\r\\n| extend RIAllocationPercentage = iif(MaxRIConsumed < VMQuantity, 1.0, 1 - (MaxRIConsumed - VMQuantity) / MaxRIConsumed)\\r\\n| project todatetime(Date_s), SavedAmount = (DailyOnDemandCost-DailyRICost) * RIAllocationPercentage\\r\\n\",\n              \"size\": 1,\n              \"title\": \"Estimated savings (in your billing currency)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavedAmount\",\n                    \"label\": \"Saved Amount\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageRISimulation\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = materialize(externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName));\\r\\nlet SelectedISFGroup = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project ISFGroup);\\r\\nlet ISFRatio = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project Ratio);\\r\\nlet TermDivider = iif('{ReservationTerm}' == '3 Years', 3, 1);\\r\\nlet VMQuantity = {VMQuantity};\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend ServiceType = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), ArmRegion=tolower(ResourceLocation_s), ServiceType, MeterId=MeterId_g, ResourceId, OnDemandPrice, SubscriptionName\\r\\n| join kind=inner ( ISFGroups ) on $left.ServiceType == $right.ArmSKUName\\r\\n| where iif('{UseISF}' == 'Yes', ISFGroup == SelectedISFGroup, ServiceType == '{VMSize}')\\r\\n| where ArmRegion == '{VMRegion}'\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationReservationsPriceV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where serviceName_s == 'Virtual Machines' and reservationTerm_s == '{ReservationTerm}'\\r\\n    | extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\\r\\n    | summarize arg_max(TimeGenerated, ReservationPrice) by ArmRegion=tolower(armRegionName_s), ServiceType=tolower(armSkuName_s)\\r\\n) on ArmRegion and ServiceType\\r\\n| order by todatetime(Date_s)\\r\\n| extend RIConsumed = row_cumsum(QtyHours * Ratio / ISFRatio / 24, prev(Date_s) != Date_s)\\r\\n| extend CutOff = iif(prev(Date_s) != Date_s or prev(RIConsumed, 1, 0) < VMQuantity, false, true)\\r\\n| where not(CutOff)\\r\\n| extend RICost = QtyHours * ReservationPrice\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| summarize DailyRICost=sum(RICost), DailyOnDemandCost=sum(OnDemandCost), MaxRIConsumed=max(RIConsumed) by Date_s\\r\\n| extend RIAllocationPercentage = iif(MaxRIConsumed < VMQuantity, 1.0, 1 - (MaxRIConsumed - VMQuantity) / MaxRIConsumed)\\r\\n| project Date_s, SavedAmount = (DailyOnDemandCost-DailyRICost) * RIAllocationPercentage, AmountWouldSpendOnDemand = DailyOnDemandCost * RIAllocationPercentage\\r\\n| extend SavedPercentage = SavedAmount / AmountWouldSpendOnDemand\\r\\n| project SavedPercentage, todatetime(Date_s)\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Estimated savings (percentage)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlanUsagePercentage\",\n                    \"label\": \"Efficiency\"\n                  }\n                ],\n                \"ySettings\": {\n                  \"numberFormatSettings\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"percent\",\n                      \"useGrouping\": true\n                    }\n                  }\n                }\n              }\n            },\n            \"name\": \"onDemandUsageRIPercentageSimulation\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = materialize(externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName));\\r\\nlet SelectedISFGroup = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project ISFGroup);\\r\\nlet ISFRatio = toscalar(ISFGroups | where ArmSKUName == '{VMSize}' | project Ratio);\\r\\nlet TermDivider = iif('{ReservationTerm}' == '3 Years', 3, 1);\\r\\nlet VMQuantity = {VMQuantity};\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| where SubscriptionName in ({Subscriptions})\\r\\n| extend ServiceType = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), ArmRegion=tolower(ResourceLocation_s), ServiceType, MeterId=MeterId_g, ResourceId, OnDemandPrice, SubscriptionName\\r\\n| join kind=inner ( ISFGroups ) on $left.ServiceType == $right.ArmSKUName\\r\\n| where iif('{UseISF}' == 'Yes', ISFGroup == SelectedISFGroup, ServiceType == '{VMSize}')\\r\\n| where ArmRegion == '{VMRegion}'\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationReservationsPriceV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where serviceName_s == 'Virtual Machines' and reservationTerm_s == '{ReservationTerm}'\\r\\n    | extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\\r\\n    | summarize arg_max(TimeGenerated, ReservationPrice) by ArmRegion=tolower(armRegionName_s), ServiceType=tolower(armSkuName_s)\\r\\n) on ArmRegion and ServiceType\\r\\n| order by todatetime(Date_s)\\r\\n| extend RIConsumed = row_cumsum(QtyHours * Ratio / ISFRatio / 24, prev(Date_s) != Date_s)\\r\\n| extend CutOff = iif(prev(Date_s) != Date_s or prev(RIConsumed, 1, 0) < VMQuantity, false, true)\\r\\n| where not(CutOff)\\r\\n| summarize MaxRIConsumed=max(RIConsumed) by todatetime(Date_s)\\r\\n| extend RIUsagePercentage = iif(MaxRIConsumed >= VMQuantity, 1.0, MaxRIConsumed / VMQuantity)\\r\\n| project-away MaxRIConsumed\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Estimated efficiency\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlanUsagePercentage\",\n                    \"label\": \"Efficiency\"\n                  },\n                  {\n                    \"seriesName\": \"RIUsagePercentage\",\n                    \"label\": \"Reservation Usage\"\n                  }\n                ],\n                \"customThresholdLine\": \"1\",\n                \"customThresholdLineStyle\": 4,\n                \"ySettings\": {\n                  \"numberFormatSettings\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"percent\",\n                      \"useGrouping\": true\n                    }\n                  }\n                }\n              }\n            },\n            \"name\": \"onDemandUsageRIEfficiencySimulation\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Reservations\"\n      },\n      \"name\": \"reservationsGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/benefits-usage.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Benefits Usage'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '4730b9ab-9f13-4c28-a999-bcce218b283d'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('benefits-usage.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/benefits-usage.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"b58b4eb8-5821-44d2-bc7e-54054df27320\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LookbackPeriod\",\n            \"label\": \"Lookback Period\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"value\": {\n              \"durationMs\": 2592000000\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters - 0\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Unless specified, usage values unit corresponds to your billing currency. **Only applies to Virtual Machines usage in Azure Global**.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"text - 7\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"If below tabs are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).\",\n        \"style\": \"warning\"\n      },\n      \"name\": \"text - 8\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"b171fed6-fb31-436a-bfab-c2a9d99bda88\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Overview\",\n            \"subTarget\": \"Overview\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"b26e72a1-167d-4449-a8b6-6665814331a3\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Reservations\",\n            \"subTarget\": \"Reservations\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"1d33e1ca-6d19-4b74-8903-00c5671b8f87\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Savings Plans\",\n            \"subTarget\": \"SavingsPlans\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"0b891e89-0bbe-41f0-bfd7-b70180ae2f22\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Spot\",\n            \"subTarget\": \"Spot\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"acf819ca-ec0e-45be-9dbc-4f5703e00c7a\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"On-Demand\",\n            \"subTarget\": \"OnDemand\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"analysisTabs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"This chart shows you what is the pricing model proportion across all your VMs usage: Reservation, Savings Plan, Spot, or On-Demand (i.e., Pay-As-You-Go). All Reservations and Savings Plan usage is presented at amortized prices.\",\n              \"style\": \"info\"\n            },\n            \"name\": \"preambleText\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(EffectivePrice_s)\\r\\n| summarize FullCost=sum(FullCost) by PricingModel, bin(todatetime(Date_s), 1d)\",\n              \"size\": 1,\n              \"title\": \"Pricing Model usage\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"Spot\",\n                    \"color\": \"greenDarkDark\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlan\",\n                    \"color\": \"yellow\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"pricingModelOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Overview\"\n      },\n      \"name\": \"overviewGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"This chart allows you to compare your Reservations usage with scenarios in which the VMs were being consumed in other pricing models and understand what you are saving with Reservations. Reservations usage is presented at Reservation prices (i.e., amortized cost). This means that, for example, if you see in the chart below 100 USD for Reservation and 120 USD for On-Demand in a specific day, you are saving 20 USD against the On-Demand price (Pay-As-You-Go). Likewise for the Reservation vs. Savings Plan comparison chart.\",\n              \"style\": \"info\"\n            },\n            \"name\": \"text - 2\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"5955658a-2b0d-4e33-b68d-a55f0792bd48\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"ReservationTerm\",\n                  \"label\": \"Reservation Term\",\n                  \"type\": 10,\n                  \"description\": \"Reservation term to get the Reservations prices from\",\n                  \"isRequired\": true,\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": []\n                  },\n                  \"jsonData\": \"[\\\"1 Year\\\", \\\"3 Years\\\"]\",\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"3 Years\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"parameters - 3\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let YearFactor = toscalar(iif('{ReservationTerm}' == '3 Years',3,1));\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'Reservation'\\r\\n| extend VmSize = tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| project Date_s, VmSize, ReservationName_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=tolower(ResourceLocation_s), ResourceId, MeterId_g\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationReservationsPriceV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where serviceName_s == 'Virtual Machines' and reservationTerm_s == '{ReservationTerm}'\\r\\n    | extend ReservationPrice = todouble(replace_string(unitPrice_s, ',', '.'))/YearFactor/12/730\\r\\n    | distinct ReservationMeter=meterName_s, VmSize=armSkuName_s, ReservationPrice, ArmRegion=tolower(armRegionName_s)\\r\\n) on VmSize and ArmRegion\\r\\n| project-away VmSize1, ArmRegion1\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice, ReservationsCost = QtyHours * ReservationPrice\\r\\n| summarize Reservation=sum(ReservationsCost), OnDemand=sum(OnDemandCost) by bin(todatetime(Date_s), 1d)\",\n              \"size\": 1,\n              \"title\": \"Reservation usage vs. On-Demand prices\",\n              \"noDataMessage\": \"There is no Reservations consumption or Pricesheet data for the selected lookback period\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"unstackedbar\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"reservationsComparisonWithOnDemand\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let YearFactor = toscalar(iif('{ReservationTerm}' == '3 Years',3,1));\\r\\nlet LinuxSavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and MeterSubCategory_s !endswith \\\"Windows\\\" and Term_s == iif('{ReservationTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet SavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{ReservationTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxSavingsPlanPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend SavingsPlanPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, SavingsPlanPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'Reservation'\\r\\n| extend VmSize = tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| project Date_s, VmSize, ReservationName_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=tolower(ResourceLocation_s), MeterId=MeterId_g, ResourceId\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationReservationsPriceV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where serviceName_s == 'Virtual Machines' and reservationTerm_s == '{ReservationTerm}'\\r\\n    | extend ReservationPrice = todouble(replace_string(unitPrice_s, ',', '.'))/YearFactor/12/730\\r\\n    | distinct ReservationMeter=meterName_s, VmSize=armSkuName_s, ReservationPrice, ArmRegion=tolower(armRegionName_s)\\r\\n) on VmSize and ArmRegion\\r\\n| project-away VmSize1, ArmRegion1\\r\\n| extend ReservationsCost = QtyHours * ReservationPrice\\r\\n| join kind=inner ( SavingsPlanPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| extend SavingsPlanCost = QtyHours * SavingsPlanPrice \\r\\n| summarize Reservation=sum(ReservationsCost), SavingsPlan=sum(SavingsPlanCost) by bin(todatetime(Date_s), 1d)\",\n              \"size\": 1,\n              \"title\": \"Reservation usage vs. Savings Plan prices\",\n              \"noDataMessage\": \"There is no Reservations consumption or Pricesheet data for the selected lookback period\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"unstackedbar\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlan\",\n                    \"color\": \"yellow\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"reservationsToSavingsPlanTradeInAnalysis\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Reservations\"\n      },\n      \"name\": \"reservationsGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"This chart allows you to compare your Savings Plans usage with scenarios in which the VMs were being consumed in other pricing models and understand what you are saving with Savings Plans. Savings Plans usage is presented at Savings Plan prices (i.e., amortized cost). This means that, for example, if you see in the chart below 100 USD for Savings Plan and 120 USD for On-Demand in a specific day, you are saving 20 USD against the On-Demand price (Pay-As-You-Go).\\r\\n\\r\\n\",\n              \"style\": \"info\"\n            },\n            \"name\": \"text - 1\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"977209de-fdec-4c84-86fd-7b0815aa71e1\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"SavingsPlanTerm\",\n                  \"label\": \"Savings Plan Term\",\n                  \"type\": 10,\n                  \"description\": \"Savings Plan term to get the Savings Plan prices from\",\n                  \"isRequired\": true,\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"jsonData\": \"[\\\"1 Year\\\", \\\"3 Years\\\"]\",\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"3 Years\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"parameters - 2\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nlet LinuxSavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and MeterSubCategory_s !endswith \\\"Windows\\\" and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet SavingsPlanPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{SavingsPlanTerm}' == '3 Years','P3Y','P1Y')\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxSavingsPlanPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend SavingsPlanPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, SavingsPlanPrice;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'SavingsPlan'\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=tolower(ResourceLocation_s), MeterId=MeterId_g, ResourceId\\r\\n| join kind=inner ( OnDemandPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| join kind=inner ( SavingsPlanPriceSheet ) on $left.MeterId == $right.MeterID_g\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice, SavingsPlanCost = QtyHours * SavingsPlanPrice\\r\\n| summarize SavingsPlan=sum(SavingsPlanCost), OnDemand=sum(OnDemandCost) by bin(todatetime(Date_s), 1d)\",\n              \"size\": 1,\n              \"title\": \"Savings Plans usage vs. On-Demand prices\",\n              \"noDataMessage\": \"There is no Savings Plans consumption or Pricesheet data for the selected lookback period\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"unstackedbar\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"SavingsPlan\",\n                    \"color\": \"yellow\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"savingsPlansComparisonWithOnDemand\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"SavingsPlans\"\n      },\n      \"name\": \"savingsPlansGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"cb40bd72-bb9a-4116-8edd-c5b3c4f2d224\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"OnDemandCostFactor\",\n                  \"label\": \"Default Price Multiplier\",\n                  \"type\": 1,\n                  \"description\": \"Price multiplier for the cases where there is not direct match between Spot meter and pricesheet meter\",\n                  \"isRequired\": true,\n                  \"isHiddenWhenLocked\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"4\"\n                }\n              ],\n              \"style\": \"pills\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"spotParameters\"\n          },\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"This chart allows you to compare your Spot usage with scenarios in which the VMs were being consumed in other pricing models and understand what you are saving with Spot. Spot usage is presented at Spot prices. This means that, for example, if you see in the chart below 100 USD for Spot and 120 USD for On-Demand in a specific day, you are saving 20 USD against the On-Demand price (Pay-As-You-Go).\",\n              \"style\": \"info\"\n            },\n            \"name\": \"text - 2\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let OnDemandCostFactor = {OnDemandCostFactor};\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'Spot'\\r\\n| extend MeterRegion = tostring(split(ProductName_s, ' - ')[2])\\r\\n| extend BillingMeter = substring(MeterName_s, 0, indexof(MeterName_s, ' Spot'))\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter, MeterRegion, ResourceId, SpotPrice=todouble(EffectivePrice_s)\\r\\n| join kind=leftouter ( \\r\\n   AzureOptimizationPricesheetV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !has 'Windows'\\r\\n    | extend UnitHrs = toint(substring(UnitOfMeasure_s, 0, indexof(UnitOfMeasure_s, 'Hour')-1))\\r\\n    | extend OnDemandPrice = todouble(UnitPrice_s)/UnitHrs\\r\\n    | summarize OnDemandPrice=max(OnDemandPrice) by BillingMeter=MeterName_s, MeterRegion=MeterRegion_s\\r\\n) on BillingMeter, MeterRegion\\r\\n| extend OnDemandCost = iif(isnotempty(OnDemandPrice), QtyHours * OnDemandPrice, QtyHours * SpotPrice * OnDemandCostFactor), SpotCost = QtyHours * SpotPrice\\r\\n| summarize Spot=sum(SpotCost), OnDemand=sum(OnDemandCost) by bin(todatetime(Date_s), 1d)\",\n              \"size\": 1,\n              \"title\": \"Spot usage vs. On-Demand prices\",\n              \"noDataMessage\": \"There is no Savings Plans consumption for the selected lookback period\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"unstackedbar\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  },\n                  {\n                    \"seriesName\": \"Spot\",\n                    \"color\": \"greenDarkDark\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"spotComparisonWithOnDemand\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Spot\"\n      },\n      \"name\": \"spotGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"Identify the potential hourly commitments for either Savings Plans or Reservations.\\r\\n\\r\\nThe first chart shows you the actual hourly cost at On-Demand prices. You can use it as a reference to compare with different Savings Plans commitment terms.\\r\\n\\r\\nFor the Savings Plan analysis, the chart shows the average hourly On-Demand (PAYG) usage in your currency (at Savings Plans prices) for each Azure subscription. It shows you the hourly amount you need to commit for (at Savings Plans prices) to cover the On-Demand usage.\\r\\n\\r\\nFor the vCPUs (Reservations) analysis, the chart shows the average hourly On-Demand (PAYG) usage in vCPUs for each VM size family. For example, if VM size family Dv5 in West Europe has 48 vCPUs of average hourly On-Demand usage, you need a Reservation commitment of 24 instances of the D2_v5 size (24x2=48 vCPUs) to fully cover its On-Demand usage with Reservation discounts.\\r\\n\",\n              \"style\": \"upsell\"\n            },\n            \"name\": \"text - 2\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"95342903-8525-4dae-af71-11eaa72be92a\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"SavingsPlanTerm\",\n                  \"label\": \"Savings Plan Term\",\n                  \"type\": 10,\n                  \"description\": \"Savings Plan term to get the Savings Plan prices from\",\n                  \"isRequired\": true,\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"jsonData\": \"[\\\"1 Year\\\", \\\"3 Years\\\"]\",\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"3 Years\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"parameters - 3\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice=todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s)), SubscriptionName\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| summarize HourlyCost=sum(OnDemandCost)/24 by bin(todatetime(Date_s), 1d), SubscriptionName\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Average On-Demand hourly usage (actual cost)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageAsIs\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=tolower(ResourceLocation_s), MeterId=MeterId_g, ResourceId, SubscriptionName\\r\\n| join kind=inner ( \\r\\n   AzureOptimizationPricesheetV1_CL\\r\\n    | where TimeGenerated > ago(14d)\\r\\n    | where MeterCategory_s == 'Virtual Machines' and PriceType_s in ('Savings Plan','SavingsPlan') and Term_s == iif('{SavingsPlanTerm}' == '3 Years', 'P3Y', 'P1Y')\\r\\n    | extend UnitHrs = toint(substring(UnitOfMeasure_s, 0, indexof(UnitOfMeasure_s, 'Hour')-1))\\r\\n    | extend SavingsPlanPrice = todouble(UnitPrice_s)/UnitHrs\\r\\n    | summarize SavingsPlanPrice=max(SavingsPlanPrice) by MeterId=MeterID_g\\r\\n) on MeterId\\r\\n| extend SavingsPlanCost = QtyHours * SavingsPlanPrice\\r\\n| summarize HourlyCost=sum(SavingsPlanCost)/24 by bin(todatetime(Date_s), 1d), SubscriptionName\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Average On-Demand hourly usage (Savings Plan prices)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageSavingsPlansPerspective\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend SubscriptionName = iif(isnotempty(SubscriptionName_s), SubscriptionName_s, SubscriptionName)\\r\\n| extend VCPUs = toint(parse_json(AdditionalInfo_s).VCPUs)\\r\\n| extend MeterRegion = tostring(split(ProductName_s, ' - ')[2])\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ArmRegion=ResourceLocation_s, MeterRegion, MeterId=MeterId_g, ResourceId, SubscriptionName, VCPUs, MeterSubCategory_s\\r\\n| extend ReservationSKU = strcat(MeterSubCategory_s, ' ', MeterRegion)\\r\\n| summarize HourlyVCPUs=sum(VCPUs*QtyHours/24) by bin(todatetime(Date_s), 1d), ReservationSKU\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Average On-Demand hourly usage (vCPUs for VM Reservations)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Reservation\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"OnDemand\",\n                    \"color\": \"red\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"onDemandUsageReservationsPerspective\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"OnDemand\"\n      },\n      \"name\": \"ondemandPlansGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/blockblobstorage-usage.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Block Blob Storage Usage'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '871eb144-c8bb-4824-90c3-f84fe197933a'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('blockblobstorage-usage.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/blockblobstorage-usage.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"8ef006f0-db8d-40b2-b51f-7a62b03e235e\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LookbackPeriod\",\n            \"label\": \"Lookback Period\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2419200000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"value\": {\n              \"durationMs\": 2592000000\n            }\n          },\n          {\n            \"id\": \"e560c503-9664-4cea-977e-8dbec62ddd64\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Subscriptions\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"resourcecontainers\\r\\n| where type == 'microsoft.resources/subscriptions'\\r\\n| project subscriptionId, name\\r\\n| order by name asc\",\n            \"crossComponentResources\": [\n              \"value::tenant\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ]\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"queryType\": 1,\n            \"resourceType\": \"microsoft.resources/tenants\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"d91ac170-ca28-403d-b06d-164b4239aaa3\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Currency\",\n            \"type\": 1,\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"value\": \"EUR\"\n          }\n        ],\n        \"style\": \"above\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"globalParameters\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| summarize ['Usage Start']=min(todatetime(Date_s)), ['Usage End']=max(todatetime(Date_s))\\r\\n| extend ['Usage Days'] = toint(format_timespan(['Usage End']-['Usage Start'],'d'))\",\n        \"size\": 4,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"table\",\n        \"tileSettings\": {\n          \"showBorder\": false\n        }\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"usagePeriod\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Only Block Blob storage usage is presented (Files, Queues, Page Blobs, etc. are not included). Costs are presented at the on-demand price, i.e., as if no Storage reservations were applied to usage.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"disclaimer\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"1dcb642e-3696-4e44-9d71-00d434709cee\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Overview\",\n            \"subTarget\": \"Overview\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"980caea9-179c-4bcb-b501-bdb68d4a578c\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Standard v2\",\n            \"subTarget\": \"StorageV2\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"642cdd5a-b00a-4713-8fcf-41da789a3a22\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Standard v1\",\n            \"subTarget\": \"StorageV1\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"ae51c121-2917-43b1-bea1-9a519847c4a9\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Premium\",\n            \"subTarget\": \"Premium\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"areaTabs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| extend StorageOffer = iif(MeterSubCategory_s == 'General Block Blob', 'Standard v1', iif(MeterSubCategory_s has 'Premium', 'Premium', 'Standard v2'))\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by StorageOffer\",\n              \"size\": 4,\n              \"title\": \"Storage Offers ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"33\",\n            \"name\": \"storageOffers\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| extend FileStructure = iif(MeterSubCategory_s has 'Hierarchical Namespace', 'HNS', 'Flat')\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by FileStructure\",\n              \"size\": 4,\n              \"title\": \"File Structure ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"33\",\n            \"name\": \"fileStructure\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| join kind=inner (StorageReplication) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by Replication\\r\\n\\r\\n\\r\\n\",\n              \"size\": 4,\n              \"title\": \"Replication ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"33\",\n            \"name\": \"replication\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nlet StorageSize = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| summarize SizeGB=sum(todouble(Quantity_s)) by ResourceId, Date_s\\r\\n| summarize arg_max(todatetime(Date_s), SizeGB) by ResourceId\\r\\n| project SizeGB=round(SizeGB*30), ResourceId;\\r\\nlet StorageTransactions = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterName_s has 'Operations'\\r\\n| summarize Transactions=round(sum(todouble(Quantity_s))*10000) by ResourceId;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| extend StorageOffer = iif(MeterSubCategory_s == 'General Block Blob', 'Standard v1', iif(MeterSubCategory_s has 'Premium', 'Premium', 'Standard v2'))\\r\\n| join kind=leftouter (StorageReplication) on ResourceId\\r\\n| join kind=leftouter (StorageSize) on ResourceId\\r\\n| join kind=leftouter (StorageTransactions) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| extend FileStructure = iif(MeterSubCategory_s has 'Hierarchical Namespace', 'HNS', 'Flat')\\r\\n| summarize ['Cost ({Currency:value})']=round(sum(FullCost)) by ['Storage Account']=ResourceId, Offer=StorageOffer, ['File Structure']=FileStructure, Replication, ['Size (GB)']=SizeGB, Transactions\\r\\n| order by ['Cost ({Currency:value})']\\r\\n\\r\\n\\r\\n\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Storage Accounts List\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Size\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Transactions\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Cost (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"name\": \"storageAccountsCosts\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Overview\"\n      },\n      \"name\": \"overviewGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| extend FileStructure = iif(MeterSubCategory_s has 'Hierarchical Namespace', 'HNS', 'Flat')\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by FileStructure\",\n              \"size\": 4,\n              \"title\": \"File Structure ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"25\",\n            \"name\": \"fileStructure\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| join kind=inner (StorageReplication) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by Replication\",\n              \"size\": 4,\n              \"title\": \"Replication ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"25\",\n            \"name\": \"replication\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored' and (MeterName_s has 'Hot' or MeterName_s has 'Cool' or MeterName_s has 'Archive')\\r\\n| extend Tier = iif(MeterName_s has 'Hot', 'Hot', iif(MeterName_s has 'Cool', 'Cool', 'Archive'))\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by Tier\",\n              \"size\": 4,\n              \"title\": \"Data Stored Tiering ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"25\",\n            \"name\": \"dataStoredTiering\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Operations' and (MeterName_s has 'Hot' or MeterName_s has 'Cool' or MeterName_s has 'Archive')\\r\\n| extend Tier = iif(MeterName_s has 'Hot', 'Hot', iif(MeterName_s has 'Cool', 'Cool', 'Archive'))\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by Tier\",\n              \"size\": 4,\n              \"title\": \"Transactions Tiering ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"25\",\n            \"name\": \"transactionsTiering\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"aa472561-a676-4733-8e13-e9ce6366a187\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Hot2CoolTarget\",\n                  \"label\": \"Hot to Cool Target (%)\",\n                  \"type\": 1,\n                  \"description\": \"The % of Hot data estimated to be moved to Cool with LCM\",\n                  \"isRequired\": true,\n                  \"value\": \"50\"\n                },\n                {\n                  \"id\": \"0344b31d-bfbe-4b4b-a3a4-63310cd5a719\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"MoveToHotThreshold\",\n                  \"label\": \"Trans. Move to Hot (%)\",\n                  \"type\": 1,\n                  \"description\": \"The max. % of transactions costs vs. total costs and of Cool transactions costs vs. total transactions costs\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"50\"\n                },\n                {\n                  \"id\": \"de446dba-7535-4cc0-ab16-23c9ae3aa9fa\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"ErrorMargin\",\n                  \"label\": \"Comfort Margin (%)\",\n                  \"type\": 1,\n                  \"description\": \"The comfort margin (%) for estimation errors (difference between Enable vs. Maybe LCM)\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"20\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"storageV2Parameters\"\n          },\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"Enable Lifecycle Management recommendations may not apply when there is a very large object count to be moved to cooler tiers. Each object moved to a cooler tier will trigger a SetBlobTier transaction billed at Cool Write Operations pricing. Please review the blob count metrics and estimate Lifecycle Management operations costs before applying the recommendation. See more details [here](https://learn.microsoft.com/en-us/azure/storage/blobs/access-tiers-overview#changing-a-blobs-access-tier). For pricing, check [here](https://azure.microsoft.com/en-us/pricing/details/storage/blobs/).\",\n              \"style\": \"warning\"\n            },\n            \"name\": \"lcmWarning\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let MoveToHotThreshold = todouble({MoveToHotThreshold})/100;\\r\\nlet Hot2CoolPercentage = todouble({Hot2CoolTarget})/100;\\r\\nlet ErrorMarginPercentage = todouble({ErrorMargin})/100;\\r\\nlet StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nlet StorageSize = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize SizeGB=sum(todouble(Quantity_s)), CostStored=sum(FullCost) by ResourceId, Date_s\\r\\n| summarize arg_max(todatetime(Date_s), SizeGB), CostStored=round(sum(CostStored)) by ResourceId\\r\\n| project SizeGB=round(SizeGB*30), CostStored, ResourceId;\\r\\nlet StorageTransactions = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Operations'\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Transactions=round(sum(todouble(Quantity_s))*10000), TransactionsCost=round(sum(FullCost)) by ResourceId;\\r\\nlet HotStorageSize = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored' and MeterName_s has 'Hot'\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize HotSizeCost=round(sum(FullCost)) by ResourceId;\\r\\nlet HotTransactions = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| where MeterName_s has 'Operations' and (MeterName_s has 'Hot' or MeterName_s has 'List and Create Container' or MeterName_s has 'All Other')\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize HotTransactions=round(sum(todouble(Quantity_s))*10000), HotTransactionsCost=round(sum(FullCost)) by ResourceId;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s !has 'Premium' and MeterSubCategory_s != 'General Block Blob'\\r\\n| join kind=leftouter (StorageReplication) on ResourceId\\r\\n| join kind=leftouter (StorageSize) on ResourceId\\r\\n| join kind=leftouter (StorageTransactions) on ResourceId\\r\\n| join kind=leftouter (HotStorageSize) on ResourceId\\r\\n| join kind=leftouter (HotTransactions) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| extend FileStructure = iif(MeterSubCategory_s has 'Hierarchical Namespace', 'HNS', 'Flat')\\r\\n| summarize TotalCost=round(sum(FullCost)) by ['Storage Account']=ResourceId, Structure=FileStructure, Replication, ['Size (GB)']=SizeGB, CostStored, HotSizeCost, Transactions, TransactionsCost, HotTransactionsCost\\r\\n| extend TransactionsFactor = round(TransactionsCost/TotalCost,1)\\r\\n| extend HotTransactionsFactor = round(HotTransactionsCost/TransactionsCost,1)\\r\\n| extend CostStored = iif(isnotempty(CostStored), CostStored, 0.0)\\r\\n| extend HotSizeCost = iif(isnotempty(HotSizeCost), HotSizeCost, 0.0)\\r\\n| extend TransactionsCost = iif(isnotempty(TransactionsCost), TransactionsCost, 0.0)\\r\\n| extend HotTransactionsCost = iif(isnotempty(HotTransactionsCost), HotTransactionsCost, 0.0)\\r\\n| extend EstimatedSavings = round(HotSizeCost*Hot2CoolPercentage/2-HotTransactionsCost*Hot2CoolPercentage*2)\\r\\n| extend EstimatedSavingsWithError = round(HotSizeCost*Hot2CoolPercentage/2*(1-ErrorMarginPercentage)-HotTransactionsCost*Hot2CoolPercentage*2)*(1+ErrorMarginPercentage)\\r\\n| extend Recommendation = iif(TransactionsFactor > MoveToHotThreshold and HotTransactionsFactor < MoveToHotThreshold, 'Move to Hot', iif(EstimatedSavingsWithError > 0, 'Enable LCM', iif(EstimatedSavings > 0, 'Maybe LCM', 'No tier change')))\\r\\n| extend EstimatedSavings = iif(Recommendation != 'Move to Hot', EstimatedSavings, round((TransactionsCost-HotTransactionsCost)/2-(CostStored-HotSizeCost)*2))\\r\\n| extend Recommendation = iif(Recommendation != 'No tier change' and EstimatedSavings <= 0, 'No tier change', Recommendation)\\r\\n| project-away HotTransactionsFactor, TransactionsFactor, EstimatedSavingsWithError\\r\\n| order by TotalCost\\r\\n| project-rename ['Total ({Currency})']=TotalCost, ['Size ({Currency})']=CostStored, ['Hot Size ({Currency})']=HotSizeCost, ['Trans. ({Currency})']=TransactionsCost, ['Hot Trans. ({Currency})']=HotTransactionsCost, ['Savings ({Currency})']=EstimatedSavings\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Storage Accounts List\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Storage Account\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"29ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Size\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Transactions\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Trans. (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Hot Trans. (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Total Cost (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Est. Savings (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Recommendation\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Move to Hot\",\n                          \"representation\": \"3\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Enable LCM\",\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Maybe LCM\",\n                          \"representation\": \"1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"stopped\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"File Structure\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"17ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Cost (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"name\": \"storageAccountsCosts\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"StorageV2\"\n      },\n      \"name\": \"storageV2Group\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s == 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where MeterSubCategory_s == 'General Block Blob'\\r\\n| join kind=inner (StorageReplication) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by Replication\\r\\n\\r\\n\\r\\n\",\n              \"size\": 1,\n              \"title\": \"Replication ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"33\",\n            \"name\": \"replication\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s == 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nlet StorageSize = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s == 'General Block Blob'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| summarize SizeGB=sum(todouble(Quantity_s)) by ResourceId, Date_s\\r\\n| summarize arg_max(todatetime(Date_s), SizeGB) by ResourceId\\r\\n| project SizeGB=round(SizeGB*30), ResourceId;\\r\\nlet StorageTransactions = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s == 'General Block Blob'\\r\\n| where MeterName_s has 'Operations'\\r\\n| summarize Transactions=round(sum(todouble(Quantity_s))*10000) by ResourceId;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s == 'General Block Blob'\\r\\n| join kind=leftouter (StorageReplication) on ResourceId\\r\\n| join kind=leftouter (StorageSize) on ResourceId\\r\\n| join kind=leftouter (StorageTransactions) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize ['Cost ({Currency:value})']=round(sum(FullCost)) by ['Storage Account']=ResourceId, Replication, ['Size (GB)']=SizeGB, Transactions\\r\\n| order by ['Cost ({Currency:value})']\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Storage Accounts List\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Size\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Transactions\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Cost (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"name\": \"storageAccountsCosts\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"StorageV1\"\n      },\n      \"name\": \"storageV1Group\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| extend FileStructure = iif(MeterSubCategory_s has 'Hierarchical Namespace', 'HNS', 'Flat')\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by FileStructure\",\n              \"size\": 4,\n              \"title\": \"File Structure ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"33\",\n            \"name\": \"fileStructure\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| join kind=inner (StorageReplication) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| summarize Cost=sum(FullCost) by Replication\",\n              \"size\": 1,\n              \"title\": \"Replication ({Currency:value})\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"33\",\n            \"name\": \"replication\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let StorageReplication = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| extend Replication = tostring(split(MeterName_s, ' ')[-3])\\r\\n| distinct ResourceId, Replication;\\r\\nlet StorageSize = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| where MeterName_s has 'Data Stored'\\r\\n| summarize SizeGB=sum(todouble(Quantity_s)) by ResourceId, Date_s\\r\\n| summarize arg_max(todatetime(Date_s), SizeGB) by ResourceId\\r\\n| project SizeGB=round(SizeGB*30), ResourceId;\\r\\nlet StorageTransactions = AzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| where MeterName_s has 'Operations'\\r\\n| summarize Transactions=round(sum(todouble(Quantity_s))*10000) by ResourceId;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) between (todatetime('{LookbackPeriod:startISO}')..todatetime('{LookbackPeriod:endISO}'))\\r\\n| where SubscriptionId in ({Subscriptions:value})\\r\\n| where MeterCategory_s == 'Storage' and (MeterSubCategory_s has 'Block Blob' or MeterSubCategory_s has 'Hierarchical Namespace' or MeterSubCategory_s has 'Blob Storage')\\r\\n| where ResourceId has 'microsoft.storage' or ResourceId has 'microsoft.classicstorage'\\r\\n| where MeterSubCategory_s has 'Premium'\\r\\n| join kind=leftouter (StorageReplication) on ResourceId\\r\\n| join kind=leftouter (StorageSize) on ResourceId\\r\\n| join kind=leftouter (StorageTransactions) on ResourceId\\r\\n| extend FullCost = todouble(Quantity_s) * todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n| extend FileStructure = iif(MeterSubCategory_s has 'Hierarchical Namespace', 'HNS', 'Flat')\\r\\n| summarize ['Cost ({Currency:value})']=round(sum(FullCost)) by ['Storage Account']=ResourceId, ['File Structure']=FileStructure, Replication, ['Size (GB)']=SizeGB, Transactions\\r\\n| order by ['Cost ({Currency:value})']\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Storage Accounts List\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Size\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Transactions\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Cost (EUR)\",\n                    \"formatter\": 2,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"name\": \"storageAccountsCosts\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Premium\"\n      },\n      \"name\": \"premiumGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/costs-growing.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Costs Growing'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '81afe6eb-8e9e-4315-811c-89b5de245c9a'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('costs-growing.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/costs-growing.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"### Outliers/growing costs conditions\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"text - 5\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"### Filters\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"text - 5 - Copy\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"017ba09e-f6c5-496e-bc96-b7cfc09cf561\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"CostTimeRange\",\n            \"label\": \"Cost Time Range\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 604800000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 1209600000\n            }\n          },\n          {\n            \"id\": \"85603c03-1d4b-474b-9cdb-9ba97192fc9e\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"InstanceMinCost\",\n            \"label\": \"Min. daily cost\",\n            \"type\": 1,\n            \"description\": \"The minimum cost to be considered for an instance to be reported\",\n            \"isRequired\": true,\n            \"value\": \"1\",\n            \"typeSettings\": {\n              \"paramValidationRules\": [\n                {\n                  \"regExp\": \"^[1-9][0-9]*$\",\n                  \"match\": true,\n                  \"message\": \"Must be an integer greater than 0\"\n                }\n              ]\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            }\n          },\n          {\n            \"id\": \"5cf46ddd-b223-4333-a025-65c79eb7ee78\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"GrowthPercentage\",\n            \"label\": \"Growth (%)\",\n            \"type\": 1,\n            \"description\": \"Cost growth from start to end date\",\n            \"isRequired\": true,\n            \"value\": \"1\",\n            \"typeSettings\": {\n              \"paramValidationRules\": [\n                {\n                  \"regExp\": \"^-?[0-9][0-9]*$\",\n                  \"match\": true,\n                  \"message\": \"Must be an integer\"\n                }\n              ]\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            }\n          },\n          {\n            \"id\": \"af718458-abd1-4608-85eb-ae60152caa13\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"TopN\",\n            \"label\": \"Top #\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"value\": \"20\",\n            \"timeContext\": {\n              \"durationMs\": 0\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"parameters - 1\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"crossComponentResources\": [\n          \"value::all\"\n        ],\n        \"parameters\": [\n          {\n            \"id\": \"b48a696e-cf64-450b-a7cc-9e0a5e457170\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"SelectedSubscriptions\",\n            \"label\": \"Subscription\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"resourcecontainers\\r\\n| where type =~ 'microsoft.resources/subscriptions'\\r\\n| project subscriptionId, name\",\n            \"crossComponentResources\": [\n              \"value::all\"\n            ],\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            },\n            \"queryType\": 1,\n            \"resourceType\": \"microsoft.resourcegraph/resources\"\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 1,\n        \"resourceType\": \"microsoft.resourcegraph/resources\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"parameters - 7\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Adjust the filters above according to what you consider being the appropriate thresholds for an abnormal consumption growth. If the first tile below (\\\"Top Growing/Outliers\\\") shows results, select each line to analyze the details of the costs growth over time for the specific line item.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"infoText\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet GrowthFactor = 1 + todouble({GrowthPercentage}) / 100;\\r\\nlet SubscriptionSeries = materialize(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and SubscriptionId in ({SelectedSubscriptions:value})\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by SubscriptionId, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by SubscriptionId\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| where StartCost >= 0 and EndCost > {InstanceMinCost} and iif(GrowthFactor > 1, EndCost > (StartCost * GrowthFactor), iif(GrowthFactor < 1, EndCost < (StartCost * GrowthFactor), EndCost == StartCost)));\\r\\nlet ResourceGroupSeries = materialize(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and isnotempty(ResourceGroup) and SubscriptionId in ({SelectedSubscriptions:value})\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by ResourceGroup, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by ResourceGroup\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| where StartCost >= 0 and EndCost > {InstanceMinCost} and iif(GrowthFactor > 1, EndCost > (StartCost * GrowthFactor), iif(GrowthFactor < 1, EndCost < (StartCost * GrowthFactor), EndCost == StartCost)));\\r\\nlet ResourceSeries = materialize(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and SubscriptionId in ({SelectedSubscriptions:value})\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by ResourceId, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by ResourceId\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| where StartCost >= 0 and EndCost > {InstanceMinCost} and iif(GrowthFactor > 1, EndCost > (StartCost * GrowthFactor), iif(GrowthFactor < 1, EndCost < (StartCost * GrowthFactor), EndCost == StartCost)));\\r\\nlet MeterCategorySeries = materialize(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and SubscriptionId in ({SelectedSubscriptions:value})\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by MeterCategory_s, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by MeterCategory_s\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| where StartCost >= 0 and EndCost > {InstanceMinCost} and iif(GrowthFactor > 1, EndCost > (StartCost * GrowthFactor), iif(GrowthFactor < 1, EndCost < (StartCost * GrowthFactor), EndCost == StartCost)));\\r\\nlet MeterSubCategorySeries = materialize(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and isnotempty(MeterSubCategory_s) and SubscriptionId in ({SelectedSubscriptions:value})\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by MeterSubCategory_s, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by MeterSubCategory_s\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| where StartCost >= 0 and EndCost > {InstanceMinCost} and iif(GrowthFactor > 1, EndCost > (StartCost * GrowthFactor), iif(GrowthFactor < 1, EndCost < (StartCost * GrowthFactor), EndCost == StartCost)));\\r\\nlet MeterNameSeries = materialize(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and SubscriptionId in ({SelectedSubscriptions:value})\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by MeterName_s, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by MeterName_s\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| where StartCost >= 0 and EndCost > {InstanceMinCost} and iif(GrowthFactor > 1, EndCost > (StartCost * GrowthFactor), iif(GrowthFactor < 1, EndCost < (StartCost * GrowthFactor), EndCost == StartCost)));\\r\\nSubscriptionSeries\\r\\n| extend outliers=series_decompose_anomalies(Cost, 1.5)\\r\\n| mvexpand outliers, Date_s\\r\\n| summarize arg_max(todatetime(Date_s), *) by SubscriptionId\\r\\n| where outliers == 1\\r\\n| extend SeriesType='Outliers', PerspectiveType = 'SubscriptionId', PerspectiveId = SubscriptionId\\r\\n| distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n| union (\\r\\n    SubscriptionSeries\\r\\n    | extend SeriesType='Growing', PerspectiveType = 'SubscriptionId', PerspectiveId = SubscriptionId\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union ( \\r\\n    ResourceGroupSeries\\r\\n    | extend outliers=series_decompose_anomalies(Cost, 1.5)\\r\\n    | mvexpand outliers, Date_s\\r\\n    | summarize arg_max(todatetime(Date_s), *) by ResourceGroup\\r\\n    | where outliers == 1\\r\\n    | extend SeriesType='Outliers', PerspectiveType = 'ResourceGroup', PerspectiveId = ResourceGroup\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union (\\r\\n    ResourceGroupSeries\\r\\n    | extend SeriesType='Growing', PerspectiveType = 'ResourceGroup', PerspectiveId = ResourceGroup\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union ( \\r\\n    ResourceSeries\\r\\n    | extend outliers=series_decompose_anomalies(Cost, 1.5)\\r\\n    | mvexpand outliers, Date_s\\r\\n    | summarize arg_max(todatetime(Date_s), *) by ResourceId\\r\\n    | where outliers == 1\\r\\n    | extend SeriesType='Outliers', PerspectiveType = 'ResourceId', PerspectiveId = ResourceId\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union (\\r\\n    ResourceSeries\\r\\n    | extend SeriesType='Growing', PerspectiveType = 'ResourceId', PerspectiveId = ResourceId\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union ( \\r\\n    MeterCategorySeries\\r\\n    | extend outliers=series_decompose_anomalies(Cost, 1.5)\\r\\n    | mvexpand outliers, Date_s\\r\\n    | summarize arg_max(todatetime(Date_s), *) by MeterCategory_s\\r\\n    | where outliers == 1\\r\\n    | extend SeriesType='Outliers', PerspectiveType = 'MeterCategory_s', PerspectiveId = MeterCategory_s\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union (\\r\\n    MeterCategorySeries\\r\\n    | extend SeriesType='Growing', PerspectiveType = 'MeterCategory_s', PerspectiveId = MeterCategory_s\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union ( \\r\\n    MeterSubCategorySeries\\r\\n    | extend outliers=series_decompose_anomalies(Cost, 1.5)\\r\\n    | mvexpand outliers, Date_s\\r\\n    | summarize arg_max(todatetime(Date_s), *) by MeterSubCategory_s\\r\\n    | where outliers == 1\\r\\n    | extend SeriesType='Outliers', PerspectiveType = 'MeterSubCategory_s', PerspectiveId = MeterSubCategory_s\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union (\\r\\n    MeterSubCategorySeries\\r\\n    | extend SeriesType='Growing', PerspectiveType = 'MeterSubCategory_s', PerspectiveId = MeterSubCategory_s\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union ( \\r\\n    MeterNameSeries\\r\\n    | extend outliers=series_decompose_anomalies(Cost, 1.5)\\r\\n    | mvexpand outliers, Date_s\\r\\n    | summarize arg_max(todatetime(Date_s), *) by MeterName_s\\r\\n    | where outliers == 1\\r\\n    | extend SeriesType='Outliers', PerspectiveType = 'MeterName_s', PerspectiveId = MeterName_s\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| union (\\r\\n    MeterNameSeries\\r\\n    | extend SeriesType='Growing', PerspectiveType = 'MeterName_s', PerspectiveId = MeterName_s\\r\\n    | distinct PerspectiveId, SeriesType, PerspectiveType, StartCost, EndCost\\r\\n)\\r\\n| extend FirstCost = round(StartCost, 0), LastCost = round(EndCost, 0)\\r\\n| distinct PerspectiveId, PerspectiveType, FirstCost, LastCost\\r\\n| top {TopN} by iif(GrowthFactor >= 1, LastCost-FirstCost, FirstCost-LastCost) desc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"title\": \"Top Growing/Outliers (daily costs)\",\n        \"noDataMessage\": \"The query returned no results. Your costs do not have anomalies and are not growing given the conditions filters above.\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"exportedParameters\": [\n          {\n            \"fieldName\": \"PerspectiveType\",\n            \"parameterName\": \"SelectedPerspective\",\n            \"parameterType\": 1\n          },\n          {\n            \"fieldName\": \"PerspectiveId\",\n            \"parameterName\": \"SelectedPerspectiveId\",\n            \"parameterType\": 1\n          }\n        ],\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"GrowingAndOutliers\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where {SelectedPerspective} in~ ('{SelectedPerspectiveId}') and TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize PerspectiveCost = sum(FinalCost) by bin(todatetime(Date_s), 1d)\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Evolution over time (select growing/outlier cost)\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"outliers\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet ContributingInstances = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by SubscriptionId, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by SubscriptionId\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| top {TopN} by iif({GrowthPercentage} >= 0, EndCost-StartCost, StartCost-EndCost)\\r\\n| project SubscriptionId;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and SubscriptionId in (ContributingInstances)\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by bin(todatetime(Date_s), 1d), SubscriptionId\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}')\\r\\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \\r\\n    | extend SubscriptionName = ContainerName_s\\r\\n    | extend SubscriptionId = SubscriptionGuid_g\\r\\n    | distinct SubscriptionId, SubscriptionName \\r\\n) on SubscriptionId \\r\\n| project-away SubscriptionId, SubscriptionId1\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Top contributing subscriptions over time\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"subscriptionDetails\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet ContributingInstances = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| extend ResourceGroup = tolower(ResourceGroup)\\r\\n| summarize InstanceCost = sum(FinalCost) by ResourceGroup, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by ResourceGroup\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| top {TopN} by iif({GrowthPercentage} >= 0, EndCost-StartCost, StartCost-EndCost)\\r\\n| project ResourceGroup;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| extend ResourceGroup = tolower(ResourceGroup)\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and ResourceGroup in (ContributingInstances)\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by bin(todatetime(Date_s), 1d), ResourceGroup\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Top contributing resource groups over time\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"resourceGroupDetails\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet ContributingInstances = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| extend ResourceId = tolower(ResourceId)\\r\\n| where {SelectedPerspective} =~ '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by ResourceId, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by ResourceId\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| top {TopN} by iif({GrowthPercentage} >= 0, EndCost-StartCost, StartCost-EndCost)\\r\\n| project ResourceId;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| extend ResourceId = tolower(ResourceId)\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and ResourceId in~ (ContributingInstances)\\r\\n| where {SelectedPerspective} =~ '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by bin(todatetime(Date_s), 1d), tostring(split(ResourceId,'/')[-1])\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Top contributing instances over time\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"instanceDetails\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet ContributingInstances = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by MeterCategory_s, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by MeterCategory_s\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| top {TopN} by iif({GrowthPercentage} >= 0, EndCost-StartCost, StartCost-EndCost)\\r\\n| project MeterCategory_s;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s in (ContributingInstances)\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by bin(todatetime(Date_s), 1d), MeterCategory_s\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Top contributing meter categories over time\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"meterCategoryDetails\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet ContributingInstances = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by MeterSubCategory_s, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by MeterSubCategory_s\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| top {TopN} by iif({GrowthPercentage} >= 0, EndCost-StartCost, StartCost-EndCost)\\r\\n| project MeterSubCategory_s;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and MeterSubCategory_s in (ContributingInstances)\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by bin(todatetime(Date_s), 1d), MeterSubCategory_s\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Top contributing meter subcategories over time\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"meterSubCategoryDetails\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let CostStartIndex = 1;\\r\\nlet ContributingInstances = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage'\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by MeterName_s, Date_s\\r\\n| order by todatetime(Date_s) asc\\r\\n| make-series Cost=sum(InstanceCost) on todatetime(Date_s) step 1d by MeterName_s\\r\\n| extend StartCost = todouble(Cost[CostStartIndex]), EndCost = todouble(Cost[array_length(Cost)-1])\\r\\n| top {TopN} by iif({GrowthPercentage} >= 0, EndCost-StartCost, StartCost-EndCost)\\r\\n| project MeterName_s;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > datetime('{CostTimeRange:startISO}') and TimeGenerated < datetime('{CostTimeRange:endISO}') and ChargeType_s == 'Usage' and MeterName_s in (ContributingInstances)\\r\\n| where {SelectedPerspective} == '{SelectedPerspectiveId}'\\r\\n| extend FinalCost = todouble(CostInBillingCurrency_s)\\r\\n| summarize InstanceCost = sum(FinalCost) by bin(todatetime(Date_s), 1d), MeterName_s\\r\\n| render timechart\",\n        \"size\": 1,\n        \"aggregation\": 5,\n        \"showAnalytics\": true,\n        \"title\": \"Top contributing meter names over time\",\n        \"timeContextFromParameter\": \"CostTimeRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"meterNameDetails\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/identities-roles.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Identities and Roles'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '4946ffbe-16c1-4a32-81a4-8ed024278ab2'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('identities-roles.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/identities-roles.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"a1de1642-eb3d-47b4-84b1-ab98bc398a5b\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"TenantId\",\n            \"label\": \"Microsoft Entra Tenant\",\n            \"type\": 2,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where Model_s == 'AzureAD'\\r\\n| distinct TenantGuid_g, Scope_s\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"tenantParam\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"For full functionality, this workbook requires Microsoft Entra ID exports. If you had not assigned the Global Reader role to the AOE managed identity yet, please do so ([see requirements](https://aka.ms/AzureOptimizationEngine/requirements)).\",\n        \"style\": \"info\"\n      },\n      \"name\": \"text - 22\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationAADObjectsV1_CL\\r\\n| where TenantGuid_g in ({TenantId})\\r\\n| distinct ObjectId_g, ObjectType_s\\r\\n| summarize count() by ObjectType_s\",\n        \"size\": 4,\n        \"title\": \"Microsoft Entra objects count (today)\",\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"piechart\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"objectTypesStats\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationAADObjectsV1_CL\\r\\n| where TenantGuid_g in ({TenantId}) and ObjectType_s == 'User'\\r\\n| distinct ObjectId_g, ObjectSubType_s\\r\\n| summarize count() by ObjectSubType_s\",\n        \"size\": 4,\n        \"title\": \"Microsoft Entra ID users count (today)\",\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"piechart\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"userObjectSubTypesStats\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where TenantGuid_g in ({TenantId}) and Model_s == 'AzureAD'\\r\\n| distinct PrincipalId_g, RoleDefinition_s\\r\\n| summarize count() by RoleDefinition_s\",\n        \"size\": 4,\n        \"title\": \"Microsoft Entra ID roles count (today)\",\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"piechart\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"aadRolesStats\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where TenantGuid_g in ({TenantId}) and Model_s == 'AzureRM'\\r\\n| distinct PrincipalId_g, RoleDefinition_s\\r\\n| summarize count() by RoleDefinition_s\",\n        \"size\": 4,\n        \"title\": \"Azure RM roles count (today)\",\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"piechart\"\n      },\n      \"customWidth\": \"50\",\n      \"name\": \"armRolesStats\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"18ec41b4-6b72-4311-ae38-28ffb61afab1\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Microsoft Entra ID Credentials\",\n            \"subTarget\": \"aadCredentials\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"bd735f44-2601-43be-b9d2-537cbce18176\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Microsoft Entra ID Roles\",\n            \"subTarget\": \"aadRoles\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"98683d29-655a-4248-8774-a5934e575579\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Azure RM Roles\",\n            \"subTarget\": \"armRoles\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"6f6a1e50-cc93-4d40-abd8-391eb282f9be\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Azure Classic Roles\",\n            \"subTarget\": \"classicRoles\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"topLevelTabs\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"21477f68-03d2-43a7-8181-1a436faa5451\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"aadRoleDefinitions\",\n            \"label\": \"Roles\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where TimeGenerated > ago(1d) and Model_s == 'AzureAD' and TenantGuid_g in ({TenantId})\\r\\n| distinct RoleDefinition_s\\r\\n| order by RoleDefinition_s asc\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"id\": \"da707803-554b-4d55-b01e-0960f764b22f\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"aadObjectType\",\n            \"label\": \"Object Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"value\": \"User\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": []\n            },\n            \"jsonData\": \"[\\\"User\\\",\\\"ServicePrincipal\\\"]\",\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          },\n          {\n            \"id\": \"c204a5b5-c895-4c66-8e6e-9067d24b546a\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"aadObjectSubType\",\n            \"label\": \"Sub Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationAADObjectsV1_CL\\r\\n| where TimeGenerated > ago(1d) and TenantGuid_g in ({TenantId}) and ObjectType_s == '{aadObjectType}'\\r\\n| distinct ObjectSubType_s\\r\\n| order by ObjectSubType_s asc\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"id\": \"ed6eb3b2-0ce6-4070-93f3-d4fe6212fe12\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"aadAssignmentType\",\n            \"label\": \"Assignment Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"value\": \"All\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[\\\"All\\\", \\\"Direct\\\", \\\"Group\\\"]\"\n          },\n          {\n            \"id\": \"d042742e-4a0c-4670-ace7-528f6862ae1a\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"aadRoleHistoryRange\",\n            \"label\": \"History Range\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 172800000\n                },\n                {\n                  \"durationMs\": 259200000\n                },\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"aadRoles\"\n      },\n      \"name\": \"aadRolesParams\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"2aeff133-7cd7-4bf9-aab9-a3131c33a1b8\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"armRoleDefinitions\",\n            \"label\": \"Roles\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where TimeGenerated > ago(1d) and Model_s in ('AzureRM') and TenantGuid_g in ({TenantId})\\r\\n| distinct RoleDefinition_s\\r\\n| order by RoleDefinition_s asc\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"id\": \"b713b16b-3d87-47e4-bfe0-e6f2ee008da4\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"armObjectType\",\n            \"label\": \"Object Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"value\": \"User\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": []\n            },\n            \"jsonData\": \"[\\\"User\\\",\\\"ServicePrincipal\\\"]\",\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          },\n          {\n            \"id\": \"c49dba90-280e-4327-9b58-b8ef6df41d36\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"armObjectSubType\",\n            \"label\": \"Sub Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationAADObjectsV1_CL\\r\\n| where TimeGenerated > ago(1d) and TenantGuid_g in ({TenantId}) and ObjectType_s == '{armObjectType}' and isnotempty(ObjectSubType_s) and isnotempty(ObjectSubType_s)\\r\\n| distinct ObjectSubType_s\\r\\n| order by ObjectSubType_s asc\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"id\": \"ed6eb3b2-0ce6-4070-93f3-d4fe6212fe12\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"armAssignmentType\",\n            \"label\": \"Assignment Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"value\": \"All\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[\\\"All\\\", \\\"Direct\\\", \\\"Group\\\"]\"\n          },\n          {\n            \"id\": \"56a07f4a-d0d5-4f5b-b07b-0e1d9dfcd91f\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"armRoleHistoryRange\",\n            \"label\": \"History Range\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 172800000\n                },\n                {\n                  \"durationMs\": 259200000\n                },\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"armRoles\"\n      },\n      \"name\": \"armRolesParams\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"4d485d7b-dc8d-403f-8103-f3bcc1c44d3f\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"classicRoleDefinitions\",\n            \"label\": \"Roles\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where TimeGenerated > ago(1d) and Model_s in ('AzureClassic') and TenantGuid_g in ({TenantId})\\r\\n| distinct RoleDefinition_s\\r\\n| order by RoleDefinition_s asc\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"id\": \"c16ec55d-c9c6-4c7d-8bee-5c5871788749\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"classicRoleHistoryRange\",\n            \"label\": \"History Range\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 172800000\n                },\n                {\n                  \"durationMs\": 259200000\n                },\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"classicRoles\"\n      },\n      \"name\": \"classicRolesParams\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"ee2235e8-eb13-4e74-9f8c-e92f2af8b0ce\",\n            \"cellValue\": \"selectedCredTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Credentials about to expire\",\n            \"subTarget\": \"expiringCreds\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"0bb5a248-8bb7-4ebf-a3ae-379b95ac8218\",\n            \"cellValue\": \"selectedCredTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Credentials not set to expire\",\n            \"subTarget\": \"notExpiringCreds\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"c9398c96-ea60-4775-b03d-eaf7e5960e22\",\n            \"cellValue\": \"selectedCredTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Credentials expired\",\n            \"subTarget\": \"expiredCreds\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"aadCredentials\"\n      },\n      \"name\": \"aadCredsTabs\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"b284dfc4-2d79-4634-8b50-37fed8a67592\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"ExpirySpan\",\n            \"label\": \"Expires in (days)\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"value\": \"30\",\n            \"typeSettings\": {\n              \"paramValidationRules\": [\n                {\n                  \"regExp\": \"^[1-9][0-9]*$\",\n                  \"match\": true,\n                  \"message\": \"Must be an integer greater than 0\"\n                }\n              ]\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"aadCredentials\"\n        },\n        {\n          \"parameterName\": \"selectedCredTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"expiringCreds\"\n        }\n      ],\n      \"name\": \"expiringCredsSpanParam\"\n    },\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"db97f60d-a481-430a-9320-da3b0f942540\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"MinExpirySpan\",\n            \"label\": \"Expires at least in (days)\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"value\": \"365\",\n            \"typeSettings\": {\n              \"paramValidationRules\": [\n                {\n                  \"regExp\": \"^[1-9][0-9]*$\",\n                  \"match\": true,\n                  \"message\": \"Must be an integer greater than 0\"\n                }\n              ]\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          }\n        ],\n        \"style\": \"pills\",\n        \"doNotRunWhenHidden\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"aadCredentials\"\n        },\n        {\n          \"parameterName\": \"selectedCredTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"notExpiringCreds\"\n        }\n      ],\n      \"name\": \"notExpiringCredsSpanParam\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let expiryInterval = {ExpirySpan}d;\\r\\nlet AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet AppsAndKeys = materialize (AADObjectsTable\\r\\n| where ObjectType_s in ('Application','ServicePrincipal')\\r\\n| where ObjectSubType_s != 'ManagedIdentity'\\r\\n| where Keys_s startswith '['\\r\\n| extend Keys = parse_json(Keys_s)\\r\\n| project-away Keys_s\\r\\n| mv-expand Keys\\r\\n| evaluate bag_unpack(Keys)\\r\\n| union ( \\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s in ('Application','ServicePrincipal')\\r\\n    | where ObjectSubType_s != 'ManagedIdentity'\\r\\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\\r\\n    | extend Keys = parse_json(Keys_s)\\r\\n    | project-away Keys_s\\r\\n    | evaluate bag_unpack(Keys)\\r\\n)\\r\\n);\\r\\nlet ExpirationInRisk = AppsAndKeys\\r\\n| where EndDate < now()+expiryInterval and EndDate > now()\\r\\n| project ApplicationId_g, KeyId, RiskDate = EndDate;\\r\\nlet NotInRisk = AppsAndKeys\\r\\n| where EndDate > now()+expiryInterval\\r\\n| project ApplicationId_g, KeyId, ComfortDate = EndDate;\\r\\nlet ApplicationsInRisk = ExpirationInRisk\\r\\n| join kind=leftouter ( NotInRisk ) on ApplicationId_g\\r\\n| where isempty(ComfortDate)\\r\\n| summarize ExpiresOn = max(RiskDate) by ApplicationId_g;\\r\\nlet ServicePrincipals = materialize(AADObjectsTable\\r\\n| where isnotempty(ObjectId_g)\\r\\n| where ObjectType_s == 'ServicePrincipal'\\r\\n| project SPNId = ObjectId_g, ApplicationId_g, PrincipalNames_s, DisplayName_s);\\r\\nlet GroupMemberships = AADObjectsTable\\r\\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n| where PrincipalNames_s startswith '['\\r\\n| extend GroupMember = parse_json(PrincipalNames_s)\\r\\n| project-away PrincipalNames_s\\r\\n| mv-expand GroupMember\\r\\n| union (\\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\\r\\n    | extend GroupMember = parse_json(PrincipalNames_s)\\r\\n    | project-away PrincipalNames_s\\r\\n)\\r\\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\\r\\nlet DirectAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    ServicePrincipals\\r\\n) on $left.PrincipalId_g == $right.SPNId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\\r\\nlet GroupAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    GroupMemberships\\r\\n    | join kind=inner ( \\r\\n        ServicePrincipals\\r\\n    ) on $left.GroupMember == $right.SPNId\\r\\n) on $left.PrincipalId_g == $right.GroupId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\\r\\nAppsAndKeys\\r\\n| join kind=inner (ApplicationsInRisk) on ApplicationId_g\\r\\n| summarize ExpiresOn = max(EndDate) by ApplicationId_g, DisplayName_s, Cloud_s, KeyType, TenantGuid_g\\r\\n| join kind=leftouter (\\r\\n    GroupAssignments\\r\\n    | union DirectAssignments\\r\\n) on ApplicationId_g\\r\\n| summarize countif(isnotempty(RoleDefinition_s)) by DisplayName_s, ExpiresOn, KeyType, TenantGuid_g, ApplicationId_g\\r\\n| order by countif_ desc, ExpiresOn asc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"ApplicationId_g\",\n        \"exportParameterName\": \"selectedApp\",\n        \"showExportToExcel\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"gridSettings\": {\n          \"formatters\": [\n            {\n              \"columnMatch\": \"countif_\",\n              \"formatter\": 18,\n              \"formatOptions\": {\n                \"thresholdsOptions\": \"icons\",\n                \"thresholdsGrid\": [\n                  {\n                    \"operator\": \">\",\n                    \"thresholdValue\": \"0\",\n                    \"representation\": \"2\",\n                    \"text\": \"{0}{1}\"\n                  },\n                  {\n                    \"operator\": \"Default\",\n                    \"thresholdValue\": null,\n                    \"representation\": \"Blank\",\n                    \"text\": \"{0}{1}\"\n                  }\n                ]\n              }\n            }\n          ],\n          \"labelSettings\": [\n            {\n              \"columnId\": \"DisplayName_s\",\n              \"label\": \"Application\"\n            },\n            {\n              \"columnId\": \"ExpiresOn\",\n              \"label\": \"Expires On\"\n            },\n            {\n              \"columnId\": \"KeyType\",\n              \"label\": \"Key Type\"\n            },\n            {\n              \"columnId\": \"TenantGuid_g\",\n              \"label\": \"Tenant ID\"\n            },\n            {\n              \"columnId\": \"ApplicationId_g\",\n              \"label\": \"Application ID\"\n            },\n            {\n              \"columnId\": \"countif_\",\n              \"label\": \"Role Assignments\"\n            }\n          ]\n        }\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"aadCredentials\"\n        },\n        {\n          \"parameterName\": \"selectedCredTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"expiringCreds\"\n        }\n      ],\n      \"name\": \"expiringCredsTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let expiryInterval = {MinExpirySpan}d;\\r\\nlet AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet AppsAndKeys = materialize (AADObjectsTable\\r\\n| where ObjectType_s in ('Application','ServicePrincipal')\\r\\n| where ObjectSubType_s != 'ManagedIdentity'\\r\\n| where Keys_s startswith '['\\r\\n| extend Keys = parse_json(Keys_s)\\r\\n| project-away Keys_s\\r\\n| mv-expand Keys\\r\\n| evaluate bag_unpack(Keys)\\r\\n| union ( \\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s in ('Application','ServicePrincipal')\\r\\n    | where ObjectSubType_s != 'ManagedIdentity'\\r\\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\\r\\n    | extend Keys = parse_json(Keys_s)\\r\\n    | project-away Keys_s\\r\\n    | evaluate bag_unpack(Keys)\\r\\n)\\r\\n);\\r\\nlet NotExpiringSoon = AppsAndKeys\\r\\n| where EndDate > now()+expiryInterval\\r\\n| project ApplicationId_g, ObjectType_s, DisplayName_s, Cloud_s, KeyType, TenantGuid_g, ExpiresOn = EndDate;\\r\\nlet ServicePrincipals = materialize(AADObjectsTable\\r\\n| where isnotempty(ObjectId_g)\\r\\n| where ObjectType_s == 'ServicePrincipal'\\r\\n| project SPNId = ObjectId_g, ApplicationId_g, PrincipalNames_s, DisplayName_s);\\r\\nlet GroupMemberships = AADObjectsTable\\r\\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n| where PrincipalNames_s startswith '['\\r\\n| extend GroupMember = parse_json(PrincipalNames_s)\\r\\n| project-away PrincipalNames_s\\r\\n| mv-expand GroupMember\\r\\n| union (\\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\\r\\n    | extend GroupMember = parse_json(PrincipalNames_s)\\r\\n    | project-away PrincipalNames_s\\r\\n)\\r\\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\\r\\nlet DirectAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    ServicePrincipals\\r\\n) on $left.PrincipalId_g == $right.SPNId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\\r\\nlet GroupAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    GroupMemberships\\r\\n    | join kind=inner ( \\r\\n        ServicePrincipals\\r\\n    ) on $left.GroupMember == $right.SPNId\\r\\n) on $left.PrincipalId_g == $right.GroupId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\\r\\nNotExpiringSoon\\r\\n| join kind=leftouter (\\r\\n    GroupAssignments\\r\\n    | union DirectAssignments\\r\\n) on ApplicationId_g\\r\\n| summarize countif(isnotempty(RoleDefinition_s)) by DisplayName_s, ExpiresOn, KeyType, TenantGuid_g, ApplicationId_g\\r\\n| order by countif_ desc, ExpiresOn desc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"ApplicationId_g\",\n        \"exportParameterName\": \"selectedApp\",\n        \"showExportToExcel\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"gridSettings\": {\n          \"formatters\": [\n            {\n              \"columnMatch\": \"countif_\",\n              \"formatter\": 18,\n              \"formatOptions\": {\n                \"thresholdsOptions\": \"icons\",\n                \"thresholdsGrid\": [\n                  {\n                    \"operator\": \">\",\n                    \"thresholdValue\": \"0\",\n                    \"representation\": \"2\",\n                    \"text\": \"{0}{1}\"\n                  },\n                  {\n                    \"operator\": \"Default\",\n                    \"thresholdValue\": null,\n                    \"representation\": \"Blank\",\n                    \"text\": \"{0}{1}\"\n                  }\n                ]\n              }\n            }\n          ],\n          \"labelSettings\": [\n            {\n              \"columnId\": \"DisplayName_s\",\n              \"label\": \"Application\"\n            },\n            {\n              \"columnId\": \"ExpiresOn\",\n              \"label\": \"Expires On\"\n            },\n            {\n              \"columnId\": \"KeyType\",\n              \"label\": \"Key Type\"\n            },\n            {\n              \"columnId\": \"TenantGuid_g\",\n              \"label\": \"Tenant ID\"\n            },\n            {\n              \"columnId\": \"ApplicationId_g\",\n              \"label\": \"Application ID\"\n            },\n            {\n              \"columnId\": \"countif_\",\n              \"label\": \"Role Assignments\"\n            }\n          ]\n        }\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"aadCredentials\"\n        },\n        {\n          \"parameterName\": \"selectedCredTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"notExpiringCreds\"\n        }\n      ],\n      \"name\": \"notExpiringCredsTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet AppsAndKeys = materialize (AADObjectsTable\\r\\n| where ObjectType_s in ('Application','ServicePrincipal')\\r\\n| where ObjectSubType_s != 'ManagedIdentity'\\r\\n| where Keys_s startswith '['\\r\\n| extend Keys = parse_json(Keys_s)\\r\\n| project-away Keys_s\\r\\n| mv-expand Keys\\r\\n| evaluate bag_unpack(Keys)\\r\\n| union ( \\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s in ('Application','ServicePrincipal')\\r\\n    | where ObjectSubType_s != 'ManagedIdentity'\\r\\n    | where isnotempty(Keys_s) and Keys_s !startswith '['\\r\\n    | extend Keys = parse_json(Keys_s)\\r\\n    | project-away Keys_s\\r\\n    | evaluate bag_unpack(Keys)\\r\\n)\\r\\n);\\r\\nlet ExpirationInRisk = AppsAndKeys\\r\\n| where EndDate < now()\\r\\n| project ApplicationId_g, KeyId, RiskDate = EndDate;\\r\\nlet NotInRisk = AppsAndKeys\\r\\n| where EndDate > now()\\r\\n| project ApplicationId_g, KeyId, ComfortDate = EndDate;\\r\\nlet ApplicationsInRisk = ExpirationInRisk\\r\\n| join kind=leftouter ( NotInRisk ) on ApplicationId_g\\r\\n| where isempty(ComfortDate)\\r\\n| summarize ExpiresOn = max(RiskDate) by ApplicationId_g;\\r\\nlet ServicePrincipals = materialize(AADObjectsTable\\r\\n| where isnotempty(ObjectId_g)\\r\\n| where ObjectType_s == 'ServicePrincipal'\\r\\n| project SPNId = ObjectId_g, ApplicationId_g, PrincipalNames_s, DisplayName_s);\\r\\nlet GroupMemberships = AADObjectsTable\\r\\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n| where PrincipalNames_s startswith '['\\r\\n| extend GroupMember = parse_json(PrincipalNames_s)\\r\\n| project-away PrincipalNames_s\\r\\n| mv-expand GroupMember\\r\\n| union (\\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\\r\\n    | extend GroupMember = parse_json(PrincipalNames_s)\\r\\n    | project-away PrincipalNames_s\\r\\n)\\r\\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\\r\\nlet DirectAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    ServicePrincipals\\r\\n) on $left.PrincipalId_g == $right.SPNId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\\r\\nlet GroupAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    GroupMemberships\\r\\n    | join kind=inner ( \\r\\n        ServicePrincipals\\r\\n    ) on $left.GroupMember == $right.SPNId\\r\\n) on $left.PrincipalId_g == $right.GroupId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\\r\\nAppsAndKeys\\r\\n| join kind=inner (ApplicationsInRisk) on ApplicationId_g\\r\\n| summarize ExpiresOn = max(EndDate) by ApplicationId_g, DisplayName_s, Cloud_s, KeyType, TenantGuid_g\\r\\n| join kind=leftouter (\\r\\n    GroupAssignments\\r\\n    | union DirectAssignments\\r\\n) on ApplicationId_g\\r\\n| summarize countif(isnotempty(RoleDefinition_s)) by DisplayName_s, ExpiresOn, KeyType, TenantGuid_g, ApplicationId_g\\r\\n| order by countif_ desc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"ApplicationId_g\",\n        \"exportParameterName\": \"selectedApp\",\n        \"showExportToExcel\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"gridSettings\": {\n          \"formatters\": [\n            {\n              \"columnMatch\": \"countif_\",\n              \"formatter\": 18,\n              \"formatOptions\": {\n                \"thresholdsOptions\": \"icons\",\n                \"thresholdsGrid\": [\n                  {\n                    \"operator\": \">\",\n                    \"thresholdValue\": \"0\",\n                    \"representation\": \"2\",\n                    \"text\": \"{0}{1}\"\n                  },\n                  {\n                    \"operator\": \"Default\",\n                    \"thresholdValue\": null,\n                    \"representation\": \"Blank\",\n                    \"text\": \"{0}{1}\"\n                  }\n                ]\n              }\n            }\n          ],\n          \"labelSettings\": [\n            {\n              \"columnId\": \"DisplayName_s\",\n              \"label\": \"Application\"\n            },\n            {\n              \"columnId\": \"ExpiresOn\",\n              \"label\": \"Expires On\"\n            },\n            {\n              \"columnId\": \"KeyType\",\n              \"label\": \"Key Type\"\n            },\n            {\n              \"columnId\": \"TenantGuid_g\",\n              \"label\": \"Tenant ID\"\n            },\n            {\n              \"columnId\": \"ApplicationId_g\",\n              \"label\": \"Application ID\"\n            },\n            {\n              \"columnId\": \"countif_\",\n              \"label\": \"Role Assignments\"\n            }\n          ]\n        }\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"aadCredentials\"\n        },\n        {\n          \"parameterName\": \"selectedCredTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"expiredCreds\"\n        }\n      ],\n      \"name\": \"expiredCredsTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet ServicePrincipals = materialize(AADObjectsTable\\r\\n| where ApplicationId_g == '{selectedApp}'\\r\\n| where isnotempty(ObjectId_g)\\r\\n| where ObjectType_s == 'ServicePrincipal'\\r\\n| project SPNId = ObjectId_g, ApplicationId_g, PrincipalNames_s, DisplayName_s);\\r\\nlet GroupMemberships = AADObjectsTable\\r\\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n| where PrincipalNames_s startswith '['\\r\\n| extend GroupMember = parse_json(PrincipalNames_s)\\r\\n| project-away PrincipalNames_s\\r\\n| mv-expand GroupMember\\r\\n| union (\\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\\r\\n    | extend GroupMember = parse_json(PrincipalNames_s)\\r\\n    | project-away PrincipalNames_s\\r\\n)\\r\\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\\r\\nlet DirectAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    ServicePrincipals\\r\\n) on $left.PrincipalId_g == $right.SPNId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\\r\\nlet GroupAssignments = RBACAssignmentsTable\\r\\n| join kind=inner (\\r\\n    GroupMemberships\\r\\n    | join kind=inner ( \\r\\n        ServicePrincipals\\r\\n    ) on $left.GroupMember == $right.SPNId\\r\\n) on $left.PrincipalId_g == $right.GroupId\\r\\n| project ApplicationId_g, PrincipalNames_s, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g;\\r\\nGroupAssignments\\r\\n| union DirectAssignments\\r\\n| distinct DisplayName_s, ApplicationId_g, RoleDefinition_s, Scope_s, Assignment, Model_s\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"title\": \"Roles assigned to application (selected above)\",\n        \"noDataMessage\": \"No roles assigned\",\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"ApplicationId_g\",\n        \"exportParameterName\": \"selectedApp\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"aadCredentials\"\n      },\n      \"name\": \"assignedRolesTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}) | where RoleDefinition_s in ({aadRoleDefinitions}));\\r\\nlet EnabledObjects = materialize(AADObjectsTable\\r\\n| where isnotempty(ObjectId_g)\\r\\n| where ObjectType_s == '{aadObjectType}' and ObjectSubType_s in ({aadObjectSubType}) and SecurityEnabled_s in ('True','N/A')\\r\\n| project ObjectId = ObjectId_g, PrincipalNames_s, DisplayName_s, ObjectSubType_s);\\r\\nlet GroupMemberships = AADObjectsTable\\r\\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n| where PrincipalNames_s startswith '['\\r\\n| extend GroupMember = parse_json(PrincipalNames_s)\\r\\n| project-away PrincipalNames_s\\r\\n| mv-expand GroupMember\\r\\n| union (\\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\\r\\n    | extend GroupMember = parse_json(PrincipalNames_s)\\r\\n    | project-away PrincipalNames_s\\r\\n)\\r\\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\\r\\nlet DirectUserAssignments = RBACAssignmentsTable\\r\\n| where Model_s == 'AzureAD'\\r\\n| join kind=inner (\\r\\n    EnabledObjects\\r\\n) on $left.PrincipalId_g == $right.ObjectId\\r\\n| project PrincipalNames_s, PrincipalId_g, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g, ObjectSubType_s;\\r\\nlet GroupUserAssignments = RBACAssignmentsTable\\r\\n| where Model_s == 'AzureAD'\\r\\n| join kind=inner (\\r\\n    GroupMemberships\\r\\n    | join kind=inner ( \\r\\n        EnabledObjects\\r\\n    ) on $left.GroupMember == $right.ObjectId\\r\\n) on $left.PrincipalId_g == $right.GroupId\\r\\n| project PrincipalNames_s, PrincipalId_g, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g, ObjectSubType_s;\\r\\nGroupUserAssignments\\r\\n| union DirectUserAssignments\\r\\n| distinct DisplayName_s, PrincipalNames_s, PrincipalId_g, ObjectSubType_s, RoleDefinition_s, Scope_s, Assignment\\r\\n| where '{aadAssignmentType}' == 'All' or Assignment startswith '{aadAssignmentType}'\\r\\n| order by PrincipalNames_s asc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"PrincipalId_g\",\n        \"exportParameterName\": \"AADPrincipalId\",\n        \"showExportToExcel\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"gridSettings\": {\n          \"formatters\": [\n            {\n              \"columnMatch\": \"PrincipalId_g\",\n              \"formatter\": 5\n            }\n          ],\n          \"rowLimit\": 1000,\n          \"filter\": true,\n          \"labelSettings\": [\n            {\n              \"columnId\": \"DisplayName_s\",\n              \"label\": \"Name\"\n            },\n            {\n              \"columnId\": \"PrincipalNames_s\",\n              \"label\": \"Principal Name\"\n            },\n            {\n              \"columnId\": \"ObjectSubType_s\",\n              \"label\": \"Sub Type\"\n            },\n            {\n              \"columnId\": \"RoleDefinition_s\",\n              \"label\": \"Role\"\n            },\n            {\n              \"columnId\": \"Scope_s\",\n              \"label\": \"Scope\"\n            }\n          ]\n        }\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"aadRoles\"\n      },\n      \"name\": \"aadRolesTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let AADObjectsTable = materialize(AzureOptimizationAADObjectsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}));\\r\\nlet RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}) | where RoleDefinition_s in ({armRoleDefinitions}));\\r\\nlet EnabledObjects = materialize(AADObjectsTable\\r\\n| where isnotempty(ObjectId_g)\\r\\n| where ObjectType_s == '{armObjectType}' and ObjectSubType_s in ({armObjectSubType}) and SecurityEnabled_s in ('True','N/A')\\r\\n| project ObjectId = ObjectId_g, PrincipalNames_s, DisplayName_s, ObjectSubType_s);\\r\\nlet GroupMemberships = AADObjectsTable\\r\\n| where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n| where PrincipalNames_s startswith '['\\r\\n| extend GroupMember = parse_json(PrincipalNames_s)\\r\\n| project-away PrincipalNames_s\\r\\n| mv-expand GroupMember\\r\\n| union (\\r\\n    AADObjectsTable\\r\\n    | where ObjectType_s == 'Group' and SecurityEnabled_s == 'True'\\r\\n    | where isnotempty(PrincipalNames_s) and PrincipalNames_s !startswith '['\\r\\n    | extend GroupMember = parse_json(PrincipalNames_s)\\r\\n    | project-away PrincipalNames_s\\r\\n)\\r\\n| project GroupId = ObjectId_g, GroupName = DisplayName_s, GroupMember = tostring(GroupMember), TenantGuid_g, Cloud_s;\\r\\nlet DirectUserAssignments = RBACAssignmentsTable\\r\\n| where Model_s == 'AzureRM'\\r\\n| join kind=inner (\\r\\n    EnabledObjects\\r\\n) on $left.PrincipalId_g == $right.ObjectId\\r\\n| project PrincipalNames_s, PrincipalId_g, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g, ObjectSubType_s;\\r\\nlet GroupUserAssignments = RBACAssignmentsTable\\r\\n| where Model_s == 'AzureRM'\\r\\n| join kind=inner (\\r\\n    GroupMemberships\\r\\n    | join kind=inner ( \\r\\n        EnabledObjects\\r\\n    ) on $left.GroupMember == $right.ObjectId\\r\\n) on $left.PrincipalId_g == $right.GroupId\\r\\n| project PrincipalNames_s, PrincipalId_g, DisplayName_s, RoleDefinition_s, Scope_s, Assignment = strcat('Group>',GroupName), Model_s, TenantGuid_g, ObjectSubType_s;\\r\\nGroupUserAssignments\\r\\n| union DirectUserAssignments\\r\\n| distinct DisplayName_s, PrincipalNames_s, PrincipalId_g, ObjectSubType_s, RoleDefinition_s, Scope_s, Assignment\\r\\n| where '{armAssignmentType}' == 'All' or Assignment startswith '{armAssignmentType}'\\r\\n| order by PrincipalNames_s asc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"PrincipalId_g\",\n        \"exportParameterName\": \"ARMPrincipalId\",\n        \"showExportToExcel\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"gridSettings\": {\n          \"formatters\": [\n            {\n              \"columnMatch\": \"PrincipalId_g\",\n              \"formatter\": 5\n            }\n          ],\n          \"rowLimit\": 5000,\n          \"filter\": true,\n          \"labelSettings\": [\n            {\n              \"columnId\": \"DisplayName_s\",\n              \"label\": \"Name\"\n            },\n            {\n              \"columnId\": \"PrincipalNames_s\",\n              \"label\": \"Principal Name\"\n            },\n            {\n              \"columnId\": \"ObjectSubType_s\",\n              \"label\": \"Sub Type\"\n            },\n            {\n              \"columnId\": \"RoleDefinition_s\",\n              \"label\": \"Role\"\n            },\n            {\n              \"columnId\": \"Scope_s\",\n              \"label\": \"Scope\"\n            }\n          ]\n        }\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"armRoles\"\n      },\n      \"name\": \"armRolesTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let RBACAssignmentsTable = materialize(AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > ago(1d) | where TenantGuid_g in ({TenantId}) | where RoleDefinition_s in ({classicRoleDefinitions}));\\r\\nlet DirectUserAssignments = RBACAssignmentsTable\\r\\n| where Model_s == 'AzureClassic'\\r\\n| project PrincipalId_s, RoleDefinition_s, Scope_s, Assignment = 'Direct', Model_s, TenantGuid_g;\\r\\nDirectUserAssignments\\r\\n| distinct PrincipalId_s, RoleDefinition_s, Scope_s, Assignment\\r\\n| order by PrincipalId_s asc\",\n        \"size\": 1,\n        \"showAnalytics\": true,\n        \"timeContext\": {\n          \"durationMs\": 86400000\n        },\n        \"exportFieldName\": \"PrincipalId_s\",\n        \"exportParameterName\": \"ClassicPrincipalId\",\n        \"showExportToExcel\": true,\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"gridSettings\": {\n          \"formatters\": [\n            {\n              \"columnMatch\": \"PrincipalId_g\",\n              \"formatter\": 5\n            }\n          ],\n          \"rowLimit\": 5000,\n          \"filter\": true,\n          \"labelSettings\": [\n            {\n              \"columnId\": \"PrincipalId_s\",\n              \"label\": \"Principal Name\"\n            },\n            {\n              \"columnId\": \"RoleDefinition_s\",\n              \"label\": \"Role\"\n            },\n            {\n              \"columnId\": \"Scope_s\",\n              \"label\": \"Scope\"\n            }\n          ]\n        }\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"classicRoles\"\n      },\n      \"name\": \"classicRolesTable\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationAADObjectsV1_CL\\r\\n| where TimeGenerated > ago(1d) and ObjectId_g == '{AADPrincipalId}'\\r\\n| project PrincipalId = ObjectId_g, DisplayName_s\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationRBACAssignmentsV1_CL\\r\\n    | where TimeGenerated {aadRoleHistoryRange:value} and Model_s == 'AzureAD'\\r\\n    | project PrincipalId = PrincipalId_g, Scope_s, RoleDefinition_s, TimeGenerated\\r\\n) on PrincipalId\\r\\n| summarize RoleFirstSeen = min(TimeGenerated), RoleLastSeen = max(TimeGenerated) by DisplayName_s, Scope_s, RoleDefinition_s\\r\\n| order by RoleLastSeen desc\",\n        \"size\": 1,\n        \"title\": \"Role history for selected principal\",\n        \"timeContextFromParameter\": \"aadRoleHistoryRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"aadRoles\"\n      },\n      \"name\": \"aadRoleHistory\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationAADObjectsV1_CL\\r\\n| where TimeGenerated > ago(1d) and ObjectId_g == '{ARMPrincipalId}'\\r\\n| project PrincipalId = ObjectId_g, DisplayName_s\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationRBACAssignmentsV1_CL\\r\\n    | where TimeGenerated {armRoleHistoryRange:value} and Model_s in ('AzureRM')\\r\\n    | project PrincipalId = PrincipalId_g, Scope_s, RoleDefinition_s, TimeGenerated\\r\\n) on PrincipalId\\r\\n| summarize RoleFirstSeen = min(TimeGenerated), RoleLastSeen = max(TimeGenerated) by DisplayName_s, Scope_s, RoleDefinition_s\\r\\n| order by RoleLastSeen desc\",\n        \"size\": 1,\n        \"title\": \"Role history for selected principal\",\n        \"timeContextFromParameter\": \"armRoleHistoryRange\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"armRoles\"\n      },\n      \"name\": \"armRoleHistory\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationRBACAssignmentsV1_CL\\r\\n| where TimeGenerated {classicRoleHistoryRange:value} and Model_s in ('AzureClassic') and PrincipalId_s == '{ClassicPrincipalId}'\\r\\n| project PrincipalId = PrincipalId_s, Scope_s, RoleDefinition_s, TimeGenerated\\r\\n| summarize RoleFirstSeen = min(TimeGenerated), RoleLastSeen = max(TimeGenerated) by PrincipalId, Scope_s, RoleDefinition_s\\r\\n| order by RoleLastSeen desc\",\n        \"size\": 1,\n        \"title\": \"Role history for selected principal\",\n        \"timeContext\": {\n          \"durationMs\": 2592000000\n        },\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"classicRoles\"\n      },\n      \"name\": \"classicRoleHistory\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/policy-compliance.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Policy Compliance'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '8fceeb3c-4c7a-4ba9-b97b-a6d9fc8dd6aa'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('policy-compliance.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/policy-compliance.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"fc2daade-225d-4204-9ade-c10f20c23fb5\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LastSubsGeneratedDateTime\",\n            \"label\": \"Subscription Names Generated On\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"query\": \"AzureOptimizationResourceContainersV1_CL\\r\\n| where TimeGenerated > ago(90d)\\r\\n| summarize max(TimeGenerated)\",\n            \"isHiddenWhenLocked\": true,\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LastPolicyGeneratedDateTime\",\n            \"label\": \"Policy States Generated On\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > ago(90d)\\r\\n| summarize max(TimeGenerated)\",\n            \"isHiddenWhenLocked\": true,\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"id\": \"76f1ffbe-3079-4782-b38c-4385d1f30690\"\n          },\n          {\n            \"id\": \"398ed1df-fb30-4ed7-a872-c1f10a752b40\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Subscription\",\n            \"type\": 2,\n            \"description\": \"The subscription filter does not impact other filters\",\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationResourceContainersV1_CL\\r\\n| where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n| where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n| project SubscriptionGuid_g, ContainerName_s\\r\\n| order by ContainerName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"selectAllValue\": \"*\",\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"e8de19a9-bd49-43c6-a0d7-5e6a3eddf3c0\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Initiative\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| distinct InitiativeId_s, InitiativeName_s\\r\\n| where isnotempty(InitiativeId_s)\\r\\n| distinct InitiativeId_s, InitiativeName_s\\r\\n| order by InitiativeName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"selectAllValue\": \"\",\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"34bfe4a4-f7d0-4970-96ba-9fae4136706b\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"FilterByInitiative\",\n            \"label\": \"Filter by Initiative\",\n            \"type\": 10,\n            \"description\": \"Whether to filter definitions and assignments by selected initiatives\",\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[\\\"Yes\\\", \\\"No\\\"]\",\n            \"value\": \"No\"\n          },\n          {\n            \"id\": \"494d3d43-f908-490a-b1cf-8f501ec873f5\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Definition\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}), true)\\r\\n| distinct DefinitionId_s, DefinitionName_s\\r\\n| distinct DefinitionId_s, DefinitionName_s\\r\\n| order by DefinitionName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"0b3fe5df-b854-42c6-8b1f-9879da99ecee\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Assignment\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}), InitiativeId_s in ({Initiative}) or DefinitionId_s in ({Definition}))\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| distinct AssignmentId_s, AssignmentName_s\\r\\n| order by AssignmentName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"adad6608-5599-40ae-8ddf-75184552e017\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Effect\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where isnotempty(Effect_s)\\r\\n| distinct Effect_s\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"894c0d85-0b62-4070-9046-8d47aceb8771\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"ComplianceState\",\n            \"label\": \"Compliance State\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| distinct ComplianceState_s\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"700ba782-ca74-4d59-b3bb-9174c6dde4df\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"TagName\",\n            \"label\": \"Tag Name\",\n            \"type\": 2,\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where isnotempty(Tags_s)\\r\\n| extend jsonTags = parse_json(Tags_s)\\r\\n| extend tagKeys = bag_keys(jsonTags)\\r\\n| mv-expand tagKey = tagKeys\\r\\n| extend tagKey = trim(' ', tostring(tagKey))\\r\\n| where tagKey !startswith 'hidden' and tagKey !startswith \\\"aks-managed\\\" and tagKey !startswith \\\"kubernetes.io\\\"\\r\\n| distinct tagKey\\r\\n| order by tagKey asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": null\n          },\n          {\n            \"id\": \"21aafeaa-f60a-4480-9335-6388b98a3296\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"TagValue\",\n            \"label\": \"Tag Value\",\n            \"type\": 2,\n            \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where isnotempty(Tags_s) and isnotempty(parse_json(Tags_s)['{TagName:label}'])\\r\\n| extend tagValue = tostring(parse_json(Tags_s)['{TagName:label}'])\\r\\n| distinct tagValue\\r\\n| order by tagValue asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": []\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": null\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters-0\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"The **Overview** tab is based on Azure Resource Graph and thus represents the _current state_. **Policy Analysis** and **Full Report** tabs are based on Log Analytics entries collected daily by the Azure Optimization Engine and thus represent the _most recent Policy states snapshot_. Tag filtering applies to Non-Compliant and Exempt states only, with the exception of the Overview tab, where all states are filterable by tag. The Excluded state is not supported in the Overview tab.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"introText\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment})\\r\\n| where (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| project resourceId, resourceGroup, subscriptionId, assignmentId, initiativeId, policyId\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize StatesCount=count() by initiativeId\",\n        \"size\": 0,\n        \"queryType\": 1,\n        \"resourceType\": \"microsoft.resources/tenants\",\n        \"crossComponentResources\": [\n          \"value::tenant\"\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"debug\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"true\"\n      },\n      \"name\": \"totalStatesByInitiative\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"0b5190ae-fdca-4672-a756-bfd3706b48f1\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Overview\",\n            \"subTarget\": \"Overview\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"3924fee1-f28a-4ab6-b169-2af239fa0e16\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Policy Analysis\",\n            \"subTarget\": \"PolicyAnalysis\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"032c17fe-3eae-4093-ba6e-e0b1ab4ba51f\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Full Report\",\n            \"subTarget\": \"FullReport\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"links - 5\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Compliant and Excluded states are aggregated and do not allow for drilling down to resources compliance details.\",\n        \"style\": \"info\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isNotEqualTo\",\n        \"value\": \"Overview\"\n      },\n      \"name\": \"compliantExplanation\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"Select the Tag Name only (leaving the Tag Value unset) to see the Non-Compliant states distribution by tag.\",\n              \"style\": \"upsell\"\n            },\n            \"name\": \"tagAggregationHint\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"\\r\\npolicyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceType = tolower(properties.resourceType)\\r\\n| extend stateWeight = tostring(properties.stateWeight)\\r\\n| extend timeGenerated = tostring(properties.timestamp)\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policyassignments'\\r\\n    | project assignmentId = tolower(id), assignmentName = tostring(properties.displayName)\\r\\n) on assignmentId\\r\\n| join kind=leftouter ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policysetdefinitions'\\r\\n    | project initiativeId = tolower(id), initiativeName = tostring(properties.displayName)\\r\\n) on initiativeId\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policydefinitions'\\r\\n    | project policyId = tolower(id), policyName = tostring(properties.displayName)\\r\\n) on policyId\\r\\n| project resourceId, resourceType, resourceGroup, subscriptionId, assignmentId, assignmentName, initiativeId, initiativeName, policyId, policyName, complianceState, stateWeight, timeGenerated\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize count() by complianceState\",\n              \"size\": 1,\n              \"title\": \"Overall Compliance (raw states)\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"piechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argComplianceSummary\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceType = tolower(properties.resourceType)\\r\\n| extend stateWeight = tostring(properties.stateWeight)\\r\\n| extend timeGenerated = tostring(properties.timestamp)\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policyassignments'\\r\\n    | project assignmentId = tolower(id), assignmentName = tostring(properties.displayName)\\r\\n) on assignmentId\\r\\n| join kind=leftouter ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policysetdefinitions'\\r\\n    | project initiativeId = tolower(id), initiativeName = tostring(properties.displayName)\\r\\n) on initiativeId\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policydefinitions'\\r\\n    | project policyId = tolower(id), policyName = tostring(properties.displayName)\\r\\n) on policyId\\r\\n| project resourceId, resourceType, resourceGroup, subscriptionId, assignmentId, assignmentName, initiativeId, initiativeName, policyId, policyName, complianceState, stateWeight, timeGenerated\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| distinct resourceId, complianceState\\r\\n| summarize complianceStates=make_list(complianceState) by resourceId\\r\\n| extend fullComplianceState = iif(complianceStates !contains 'NonCompliant', iif(complianceStates contains 'Exempt' and array_length(complianceStates) == 1, 'Exempt', 'Compliant'), 'NonCompliant')\\r\\n| summarize dcount(resourceId) by fullComplianceState\",\n              \"size\": 1,\n              \"title\": \"Resource Compliance\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"piechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argComplianceSummaryResources\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState == 'NonCompliant' and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceType = tolower(properties.resourceType)\\r\\n| extend stateWeight = tostring(properties.stateWeight)\\r\\n| extend timeGenerated = tostring(properties.timestamp)\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| project resourceId, resourceType, resourceGroup, subscriptionId, assignmentId, initiativeId, policyId, complianceState, stateWeight, timeGenerated\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| distinct resourceId, subscriptionId\\r\\n| summarize dcount(resourceId) by subscriptionId\\r\\n| join kind=leftouter (\\r\\n    resourcecontainers\\r\\n    | where type == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=name, subscriptionId\\r\\n) on subscriptionId\\r\\n| project-away subscriptionId, subscriptionId1\",\n              \"size\": 1,\n              \"title\": \"Non-compliant resources by Subscription\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"piechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argNonComplianceBySubscriptionSummaryResources\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState == 'NonCompliant' and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceType = tolower(properties.resourceType)\\r\\n| extend stateWeight = tostring(properties.stateWeight)\\r\\n| extend timeGenerated = tostring(properties.timestamp)\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| project resourceId, resourceType, resourceGroup, subscriptionId, assignmentId, initiativeId, policyId, complianceState, stateWeight, timeGenerated\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>', true, iif('{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}'))\\r\\n| extend TagValue = iif('{TagName:label}' == '<unset>', 'N/A', tostring(tags['{TagName:label}']))\\r\\n| distinct resourceId, TagValue\\r\\n| summarize dcount(resourceId) by TagValue\",\n              \"size\": 1,\n              \"title\": \"Non-compliant resources by Tag\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"piechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argNonComplianceByTagSummaryResources\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| join kind=leftouter ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policysetdefinitions'\\r\\n    | project initiativeId = tolower(id), initiativeName = tostring(properties.displayName)\\r\\n) on initiativeId\\r\\n| where isnotempty(initiativeName)\\r\\n| project resourceId, resourceGroup, subscriptionId, assignmentId, initiativeId, initiativeName, policyId, complianceState\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| distinct resourceId, complianceState, initiativeName\\r\\n| summarize complianceStates=make_list(complianceState) by resourceId, initiativeName\\r\\n| extend fullComplianceState = iif(complianceStates !contains 'NonCompliant', iif(complianceStates contains 'Exempt' and array_length(complianceStates) == 1, 'Exempt', 'Compliant'), 'NonCompliant')\\r\\n| summarize StatesCount=dcount(resourceId), CompliantCount=dcountif(resourceId, fullComplianceState == 'Compliant'), NonCompliantCount=dcountif(resourceId, fullComplianceState == 'NonCompliant'), ExemptCount=dcountif(resourceId, fullComplianceState == 'Exempt') by initiativeName\\r\\n| project initiativeName, CompliantPercent = round(todouble(CompliantCount)/StatesCount*100), NonCompliantPercent = round(todouble(NonCompliantCount)/StatesCount*100), ExemptPercent = round(todouble(ExemptCount)/StatesCount*100)\\r\\n| order by NonCompliantPercent, initiativeName asc\",\n              \"size\": 0,\n              \"title\": \"Resource Compliance (by Initiative)\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"CompliantPercent\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"75\",\n                          \"representation\": \"Sev4\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"50\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"NonCompliantPercent\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"50\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"25\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"Sev4\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ExemptPercent\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"initiativeName\",\n                    \"label\": \"Initiative\"\n                  },\n                  {\n                    \"columnId\": \"CompliantPercent\",\n                    \"label\": \"Compliant\"\n                  },\n                  {\n                    \"columnId\": \"NonCompliantPercent\",\n                    \"label\": \"Non Compliant\"\n                  },\n                  {\n                    \"columnId\": \"ExemptPercent\",\n                    \"label\": \"Exempt\"\n                  }\n                ]\n              },\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argComplianceByInitiativePercentageResources\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| join kind=leftouter ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policydefinitions'\\r\\n    | project policyId = tolower(id), definitionName = tostring(properties.displayName)\\r\\n) on policyId\\r\\n| project resourceId, resourceGroup, subscriptionId, assignmentId, initiativeId, policyId, definitionName, complianceState\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize StatesCount=count(), CompliantCount=countif(complianceState == 'Compliant'), NonCompliantCount=countif(complianceState == 'NonCompliant'), ExemptCount=countif(complianceState == 'Exempt') by definitionName\\r\\n| project definitionName, CompliantPercent = round(todouble(CompliantCount)/StatesCount*100), NonCompliantPercent = round(todouble(NonCompliantCount)/StatesCount*100), ExemptPercent = round(todouble(ExemptCount)/StatesCount*100)\\r\\n| order by NonCompliantPercent, definitionName asc\",\n              \"size\": 0,\n              \"title\": \"Resource Compliance (by Definition)\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"CompliantPercent\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"75\",\n                          \"representation\": \"Sev4\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"50\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"NonCompliantPercent\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"50\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \">\",\n                          \"thresholdValue\": \"25\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"Sev4\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ExemptPercent\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000,\n                \"sortBy\": [\n                  {\n                    \"itemKey\": \"$gen_thresholds_NonCompliantPercent_2\",\n                    \"sortOrder\": 2\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"definitionName\",\n                    \"label\": \"Definition\"\n                  },\n                  {\n                    \"columnId\": \"CompliantPercent\",\n                    \"label\": \"Compliant\"\n                  },\n                  {\n                    \"columnId\": \"NonCompliantPercent\",\n                    \"label\": \"Non Compliant\"\n                  },\n                  {\n                    \"columnId\": \"ExemptPercent\",\n                    \"label\": \"Exempt\"\n                  }\n                ]\n              },\n              \"sortBy\": [\n                {\n                  \"itemKey\": \"$gen_thresholds_NonCompliantPercent_2\",\n                  \"sortOrder\": 2\n                }\n              ],\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argComplianceByDefinitionPercentage\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState == 'NonCompliant' and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceType = tolower(properties.resourceType)\\r\\n| extend stateWeight = tostring(properties.stateWeight)\\r\\n| extend timeGenerated = tostring(properties.timestamp)\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policyassignments'\\r\\n    | project assignmentId = tolower(id), assignmentName = tostring(properties.displayName)\\r\\n) on assignmentId\\r\\n| join kind=leftouter ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policysetdefinitions'\\r\\n    | project initiativeId = tolower(id), initiativeName = tostring(properties.displayName)\\r\\n) on initiativeId\\r\\n| where isnotempty(initiativeName)\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policydefinitions'\\r\\n    | project policyId = tolower(id), policyName = tostring(properties.displayName)\\r\\n) on policyId\\r\\n| project resourceId, resourceType, resourceGroup, subscriptionId, assignmentId, assignmentName, initiativeId, initiativeName, policyId, policyName, complianceState, stateWeight, timeGenerated\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| distinct resourceId, initiativeName\\r\\n| summarize dcount(resourceId) by initiativeName\\r\\n| order by dcount_resourceId\",\n              \"size\": 0,\n              \"title\": \"Non-compliant resources by Initiative\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"gridSettings\": {\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"initiativeName\",\n                    \"label\": \"Initiative\"\n                  },\n                  {\n                    \"columnId\": \"dcount_resourceId\",\n                    \"label\": \"Resource #\"\n                  }\n                ]\n              },\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argComplianceByInitiativeResources\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"policyresources\\r\\n| where type == 'microsoft.policyinsights/policystates'\\r\\n| extend assignmentId = tolower(properties.policyAssignmentId)\\r\\n| extend initiativeId = tolower(properties.policySetDefinitionId)\\r\\n| extend policyId = tolower(properties.policyDefinitionId)\\r\\n| where iif('{FilterByInitiative}' == 'Yes', initiativeId in ({Initiative}) and policyId in ({Definition}) and assignmentId in ({Assignment}), policyId in ({Definition}) and assignmentId in ({Assignment}))\\r\\n| extend complianceState = tostring(properties.complianceState)\\r\\n| where complianceState == 'NonCompliant' and (\\\"{Subscription}\\\" == \\\"'*'\\\" or subscriptionId in ({Subscription}))\\r\\n| extend resourceType = tolower(properties.resourceType)\\r\\n| extend stateWeight = tostring(properties.stateWeight)\\r\\n| extend timeGenerated = tostring(properties.timestamp)\\r\\n| extend resourceId = tolower(properties.resourceId)\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policyassignments'\\r\\n    | project assignmentId = tolower(id), assignmentName = tostring(properties.displayName)\\r\\n) on assignmentId\\r\\n| join kind=leftouter ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policysetdefinitions'\\r\\n    | project initiativeId = tolower(id), initiativeName = tostring(properties.displayName)\\r\\n) on initiativeId\\r\\n| join kind=inner ( \\r\\n    policyresources\\r\\n    | where type == 'microsoft.authorization/policydefinitions'\\r\\n    | project policyId = tolower(id), policyName = tostring(properties.displayName)\\r\\n) on policyId\\r\\n| project resourceId, resourceType, resourceGroup, subscriptionId, assignmentId, assignmentName, initiativeId, initiativeName, policyId, policyName, complianceState, stateWeight, timeGenerated\\r\\n| join kind=leftouter (\\r\\n    resources\\r\\n    | project resourceId=tolower(id), tags\\r\\n) on resourceId\\r\\n| project-away resourceId1\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(tags['{TagName:label}']) and tostring(tags['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize ['Resource #']=dcount(resourceId) by policyName\\r\\n| order by ['Resource #']\",\n              \"size\": 0,\n              \"title\": \"Non-compliant resources by Definition\",\n              \"queryType\": 1,\n              \"resourceType\": \"microsoft.resources/tenants\",\n              \"crossComponentResources\": [\n                \"value::tenant\"\n              ],\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"policyName\",\n                    \"label\": \"Definition\"\n                  },\n                  {\n                    \"columnId\": \"count_\",\n                    \"label\": \"Resource #\"\n                  }\n                ]\n              },\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Compliant\",\n                    \"color\": \"green\"\n                  },\n                  {\n                    \"seriesName\": \"Excluded\",\n                    \"color\": \"gray\"\n                  },\n                  {\n                    \"seriesName\": \"NonCompliant\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Exempt\",\n                    \"color\": \"turquoise\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"argComplianceByDefinition\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Overview\"\n      },\n      \"name\": \"overviewGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}) and DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}), DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}))\\r\\n| where ComplianceState_s == 'Excluded' and 'Excluded' in ({ComplianceState})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| extend SubscriptionGuid_g = iif(ResourceId startswith '/subscriptions/', tostring(split(ResourceId,'/')[2]), '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project SubscriptionGuid_g, SubscriptionResourceCount=ResourceCount_s\\r\\n    )\\r\\n    on SubscriptionGuid_g\\r\\n| extend ManagementGroupId = iif(isempty(SubscriptionGuid_g),ResourceId, '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | mv-expand MGAncestor = parse_json(ContainerProperties_s).managementGroupAncestorsChain\\r\\n    | extend ManagementGroupId = tolower(strcat('/providers/microsoft.management/managementgroups/',MGAncestor.name))\\r\\n    | project SubscriptionIdForMG=SubscriptionGuid_g, ManagementGroupId, MGSubscriptionResourceCount=ResourceCount_s\\r\\n) on ManagementGroupId\\r\\n| extend SubscriptionGuid_g = iif(isempty(SubscriptionGuid_g), SubscriptionIdForMG, SubscriptionGuid_g)\\r\\n| where (\\\"{Subscription}\\\" == \\\"'*'\\\" or SubscriptionGuid_g in ({Subscription}))\\r\\n| extend ResourceGroupId = ResourceId\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions/resourcegroups'\\r\\n    | extend ResourceGroupId = InstanceId_s\\r\\n    | project ResourceGroupId, ResourceGroupResourceCount=ResourceCount_s\\r\\n) on ResourceGroupId\\r\\n| extend ResourceCount = iif(ResourceId !has 'resourcegroups', iif(ResourceId has 'subscriptions', toint(SubscriptionResourceCount), toint(MGSubscriptionResourceCount)), iif(isnotempty(ResourceGroupResourceCount), toint(ResourceGroupResourceCount), 1))\\r\\n| summarize StatesCount=sum(ResourceCount) by SubscriptionGuid_g, InitiativeName_s, AssignmentName_s, DefinitionName_s, Effect_s, ComplianceState_s;\\r\\nlet AllButCompliantOrExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}) and DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}), DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}))\\r\\n| where Effect_s in ({Effect}) and ComplianceState_s in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or SubscriptionGuid_g in ({Subscription}))\\r\\n| where ComplianceState_s !in ('Compliant','Excluded')\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize StatesCount=count() by SubscriptionGuid_g, InitiativeName_s, AssignmentName_s, DefinitionName_s, Effect_s, ComplianceState_s;\\r\\nAzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}) and DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}), DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}))\\r\\n| where Effect_s in ({Effect}) and ComplianceState_s in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or SubscriptionGuid_g in ({Subscription}))\\r\\n| where ComplianceState_s == 'Compliant'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| project SubscriptionGuid_g, InitiativeName_s, AssignmentName_s, DefinitionName_s, Effect_s, ComplianceState_s, StatesCount=tolong(StatesCount_s)\\r\\n| union (AllButCompliantOrExcludedStates)\\r\\n| union (ExcludedStates)\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h \\r\\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \\r\\n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s\\r\\n) on SubscriptionGuid_g\\r\\n| project-away SubscriptionGuid_g*\\r\\n| order by InitiativeName_s asc, DefinitionName_s asc, SubscriptionName asc\\r\\n| project-reorder DefinitionName_s, InitiativeName_s, AssignmentName_s, SubscriptionName, Effect_s, ComplianceState_s, StatesCount\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Policy Compliance (select line to analyze further)\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"DefinitionName_s\",\n                  \"parameterName\": \"selectedDefinition\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"InitiativeName_s\",\n                  \"parameterName\": \"selectedInitiative\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"AssignmentName_s\",\n                  \"parameterName\": \"selectedAssignment\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"SubscriptionName\",\n                  \"parameterName\": \"selectedSubscription\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"ComplianceState_s\",\n                  \"parameterName\": \"selectedState\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"DefinitionName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"InitiativeName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AssignmentName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ComplianceState_s\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Compliant\",\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"NonCompliant\",\n                          \"representation\": \"error\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Exempt\",\n                          \"representation\": \"info\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionGuid_g\",\n                    \"formatter\": 15,\n                    \"formatOptions\": {\n                      \"linkTarget\": null,\n                      \"showIcon\": true\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"DefinitionName_s\",\n                    \"label\": \"Definition\"\n                  },\n                  {\n                    \"columnId\": \"InitiativeName_s\",\n                    \"label\": \"Initiative\"\n                  },\n                  {\n                    \"columnId\": \"AssignmentName_s\",\n                    \"label\": \"Assignment\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"Effect_s\",\n                    \"label\": \"Effect\"\n                  },\n                  {\n                    \"columnId\": \"ComplianceState_s\",\n                    \"label\": \"Compliance\"\n                  },\n                  {\n                    \"columnId\": \"StatesCount\",\n                    \"label\": \"Resources #\"\n                  }\n                ]\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PolicyAnalysis\"\n            },\n            \"name\": \"mainQuery\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where InitiativeName_s == '{selectedInitiative}' and DefinitionName_s == '{selectedDefinition}' and AssignmentName_s == '{selectedAssignment}' and ComplianceState_s == 'Excluded'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| extend SubscriptionGuid_g = iif(ResourceId startswith '/subscriptions/', tostring(split(ResourceId,'/')[2]), '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project SubscriptionGuid_g, SubscriptionResourceCount=ResourceCount_s\\r\\n    )\\r\\n    on SubscriptionGuid_g\\r\\n| extend ManagementGroupId = iif(isempty(SubscriptionGuid_g),ResourceId, '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | mv-expand MGAncestor = parse_json(ContainerProperties_s).managementGroupAncestorsChain\\r\\n    | extend ManagementGroupId = tolower(strcat('/providers/microsoft.management/managementgroups/',MGAncestor.name))\\r\\n    | project SubscriptionIdForMG=SubscriptionGuid_g, ManagementGroupId, MGSubscriptionResourceCount=ResourceCount_s\\r\\n) on ManagementGroupId\\r\\n| extend SubscriptionGuid_g = iif(isempty(SubscriptionGuid_g), SubscriptionIdForMG, SubscriptionGuid_g)\\r\\n| extend ResourceGroupId = ResourceId\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions/resourcegroups'\\r\\n    | extend ResourceGroupId = InstanceId_s\\r\\n    | project ResourceGroupId, ResourceGroupResourceCount=ResourceCount_s\\r\\n) on ResourceGroupId\\r\\n| extend ResourceCount = iif(ResourceId !has 'resourcegroups', iif(ResourceId has 'subscriptions', toint(SubscriptionResourceCount), toint(MGSubscriptionResourceCount)), iif(isnotempty(ResourceGroupResourceCount), toint(ResourceGroupResourceCount), 1));\\r\\nlet AllButCompliantOrExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where InitiativeName_s == '{selectedInitiative}' and DefinitionName_s == '{selectedDefinition}' and AssignmentName_s == '{selectedAssignment}' and ComplianceState_s !in ('Compliant','Excluded')\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}');\\r\\nAzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where InitiativeName_s == '{selectedInitiative}' and DefinitionName_s == '{selectedDefinition}' and AssignmentName_s == '{selectedAssignment}' and ComplianceState_s == 'Compliant'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| union (AllButCompliantOrExcludedStates)\\r\\n| union (ExcludedStates)\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h \\r\\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \\r\\n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s\\r\\n) on SubscriptionGuid_g\\r\\n| project-away SubscriptionGuid_g*\\r\\n| where SubscriptionName == '{selectedSubscription}' and ComplianceState_s == '{selectedState}'\\r\\n| extend ResourceId = iif(ResourceId has 'policyinsights',substring(ResourceId,0,indexof(ResourceId,'/providers/microsoft.policyinsights/')),ResourceId)\\r\\n| extend ResourceCount = iif(isnotempty(ResourceCount), ResourceCount, iif(isnotempty(StatesCount_s), toint(StatesCount_s), 1))\\r\\n| project ResourceId, SubscriptionName, DefinitionName_s, AssignmentName_s, Effect_s, ComplianceState_s, ComplianceReason_s, ResourceCount\",\n              \"size\": 1,\n              \"showAnalytics\": true,\n              \"title\": \"Resources compliance (select a line from above)\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"DefinitionName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AssignmentName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ComplianceState_s\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Compliant\",\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"NonCompliant\",\n                          \"representation\": \"error\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Exempt\",\n                          \"representation\": \"info\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"InitiativeName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionGuid_g\",\n                    \"formatter\": 15,\n                    \"formatOptions\": {\n                      \"linkTarget\": null,\n                      \"showIcon\": true\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"DefinitionName_s\",\n                    \"label\": \"Definition\"\n                  },\n                  {\n                    \"columnId\": \"AssignmentName_s\",\n                    \"label\": \"Assignment\"\n                  },\n                  {\n                    \"columnId\": \"Effect_s\",\n                    \"label\": \"Effect\"\n                  },\n                  {\n                    \"columnId\": \"ComplianceState_s\",\n                    \"label\": \"Compliance\"\n                  },\n                  {\n                    \"columnId\": \"ComplianceReason_s\",\n                    \"label\": \"Reason\"\n                  }\n                ]\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PolicyAnalysis\"\n            },\n            \"name\": \"complianceDetails\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"5cc0b316-c55e-4afa-9b02-6fdeeec10ed0\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"policyHistoryRange\",\n                  \"label\": \"Policy History Time Range\",\n                  \"type\": 4,\n                  \"value\": {\n                    \"durationMs\": 604800000\n                  },\n                  \"typeSettings\": {\n                    \"selectableValues\": [\n                      {\n                        \"durationMs\": 604800000\n                      },\n                      {\n                        \"durationMs\": 1209600000\n                      },\n                      {\n                        \"durationMs\": 2419200000\n                      },\n                      {\n                        \"durationMs\": 2592000000\n                      },\n                      {\n                        \"durationMs\": 5184000000\n                      },\n                      {\n                        \"durationMs\": 7776000000\n                      }\n                    ],\n                    \"allowCustom\": true\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  }\n                }\n              ],\n              \"style\": \"pills\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PolicyAnalysis\"\n            },\n            \"name\": \"parameters-1\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{policyHistoryRange:startISO}') and TimeGenerated < todatetime('{policyHistoryRange:endISO}')\\r\\n| where InitiativeName_s == '{selectedInitiative}' and DefinitionName_s == '{selectedDefinition}' and AssignmentName_s == '{selectedAssignment}' and ComplianceState_s == 'Excluded'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| extend SubscriptionGuid_g = iif(ResourceId startswith '/subscriptions/', tostring(split(ResourceId,'/')[2]), '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project SubscriptionGuid_g, SubscriptionResourceCount=ResourceCount_s\\r\\n    )\\r\\n    on SubscriptionGuid_g\\r\\n| extend ManagementGroupId = iif(isempty(SubscriptionGuid_g),ResourceId, '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | mv-expand MGAncestor = parse_json(ContainerProperties_s).managementGroupAncestorsChain\\r\\n    | extend ManagementGroupId = tolower(strcat('/providers/microsoft.management/managementgroups/',MGAncestor.name))\\r\\n    | project SubscriptionIdForMG=SubscriptionGuid_g, ManagementGroupId, MGSubscriptionResourceCount=ResourceCount_s\\r\\n) on ManagementGroupId\\r\\n| extend SubscriptionGuid_g = iif(isempty(SubscriptionGuid_g), SubscriptionIdForMG, SubscriptionGuid_g)\\r\\n| extend ResourceGroupId = ResourceId\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions/resourcegroups'\\r\\n    | extend ResourceGroupId = InstanceId_s\\r\\n    | project ResourceGroupId, ResourceGroupResourceCount=ResourceCount_s\\r\\n) on ResourceGroupId\\r\\n| extend ResourceCount = iif(ResourceId !has 'resourcegroups', iif(ResourceId has 'subscriptions', toint(SubscriptionResourceCount), toint(MGSubscriptionResourceCount)), iif(isnotempty(ResourceGroupResourceCount), toint(ResourceGroupResourceCount), 1))\\r\\n| distinct TimeGenerated, ResourceId, SubscriptionGuid_g, DefinitionName_s, Effect_s, ComplianceState_s, ResourceCount;\\r\\nlet AllButCompliantOrExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{policyHistoryRange:startISO}') and TimeGenerated < todatetime('{policyHistoryRange:endISO}')\\r\\n| where InitiativeName_s == '{selectedInitiative}' and DefinitionName_s == '{selectedDefinition}' and AssignmentName_s == '{selectedAssignment}' and ComplianceState_s !in ('Compliant','Excluded') and ComplianceState_s == '{selectedState}'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}');\\r\\nAzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{policyHistoryRange:startISO}') and TimeGenerated < todatetime('{policyHistoryRange:endISO}')\\r\\n| where InitiativeName_s == '{selectedInitiative}' and DefinitionName_s == '{selectedDefinition}' and AssignmentName_s == '{selectedAssignment}' and ComplianceState_s == 'Compliant' and ComplianceState_s == '{selectedState}'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| union (ExcludedStates)\\r\\n| union (AllButCompliantOrExcludedStates)\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h \\r\\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \\r\\n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s\\r\\n) on SubscriptionGuid_g\\r\\n| project-away SubscriptionGuid_g*\\r\\n| where SubscriptionName == '{selectedSubscription}' and ComplianceState_s == '{selectedState}'\\r\\n| extend ResourceId = iif(ResourceId has 'policyinsights',substring(ResourceId,0,indexof(ResourceId,'/providers/microsoft.policyinsights/')),ResourceId)\\r\\n| extend ResourceCount = iif(isnotempty(ResourceCount), ResourceCount, iif(isnotempty(StatesCount_s), toint(StatesCount_s), 1))\\r\\n| distinct TimeGenerated, ResourceId, SubscriptionName, DefinitionName_s, Effect_s, ComplianceState_s, ResourceCount\\r\\n| summarize sum(ResourceCount) by bin(TimeGenerated, 1d)\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Compliance over time (select a line from top grid)\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"timechart\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"DefinitionName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ComplianceState_s\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Compliant\",\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"NonCompliant\",\n                          \"representation\": \"error\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Exempt\",\n                          \"representation\": \"info\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"InitiativeName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AssignmentName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionGuid_g\",\n                    \"formatter\": 15,\n                    \"formatOptions\": {\n                      \"linkTarget\": null,\n                      \"showIcon\": true\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PolicyAnalysis\"\n            },\n            \"name\": \"complianceOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"PolicyAnalysis\"\n      },\n      \"name\": \"policyAnalysisGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"35390c0a-62fa-4561-8537-0171b41c5a47\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"FRTagName\",\n                  \"label\": \"Group by Tag Name\",\n                  \"type\": 2,\n                  \"query\": \"AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where isnotempty(Tags_s)\\r\\n| extend jsonTags = parse_json(Tags_s)\\r\\n| extend tagKeys = bag_keys(jsonTags)\\r\\n| mv-expand tagKey = tagKeys\\r\\n| extend tagKey = trim(' ', tostring(tagKey))\\r\\n| where tagKey !startswith 'hidden' and tagKey !startswith \\\"aks-managed\\\" and tagKey !startswith \\\"kubernetes.io\\\"\\r\\n| distinct tagKey\\r\\n| order by tagKey asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": null\n                }\n              ],\n              \"style\": \"pills\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"fullReportParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}) and DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}), DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}))\\r\\n| where ComplianceState_s == 'Excluded' and 'Excluded' in ({ComplianceState})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| extend SubscriptionGuid_g = iif(ResourceId startswith '/subscriptions/', tostring(split(ResourceId,'/')[2]), '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project SubscriptionGuid_g, SubscriptionResourceCount=ResourceCount_s\\r\\n    )\\r\\n    on SubscriptionGuid_g\\r\\n| extend ManagementGroupId = iif(isempty(SubscriptionGuid_g),ResourceId, '')\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | mv-expand MGAncestor = parse_json(ContainerProperties_s).managementGroupAncestorsChain\\r\\n    | extend ManagementGroupId = tolower(strcat('/providers/microsoft.management/managementgroups/',MGAncestor.name))\\r\\n    | project SubscriptionIdForMG=SubscriptionGuid_g, ManagementGroupId, MGSubscriptionResourceCount=ResourceCount_s\\r\\n) on ManagementGroupId\\r\\n| extend SubscriptionGuid_g = iif(isempty(SubscriptionGuid_g), SubscriptionIdForMG, SubscriptionGuid_g)\\r\\n| where (\\\"{Subscription}\\\" == \\\"'*'\\\" or SubscriptionGuid_g in ({Subscription}))\\r\\n| extend ResourceGroupId = ResourceId\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions/resourcegroups'\\r\\n    | extend ResourceGroupId = InstanceId_s\\r\\n    | project ResourceGroupId, ResourceGroupResourceCount=ResourceCount_s\\r\\n) on ResourceGroupId\\r\\n| extend ResourceCount = iif(ResourceId !has 'resourcegroups', iif(ResourceId has 'subscriptions', toint(SubscriptionResourceCount), toint(MGSubscriptionResourceCount)), iif(isnotempty(ResourceGroupResourceCount), toint(ResourceGroupResourceCount), 1))\\r\\n| project ResourceId, SubscriptionGuid_g, DefinitionName_s, InitiativeName_s, AssignmentName_s, Effect_s, ComplianceState_s, ComplianceReason_s, ResourceCount, GroupByTag='';\\r\\nlet AllButCompliantOrExcludedStates = AzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where iif('{FilterByInitiative}' == 'Yes', InitiativeId_s in ({Initiative}) and DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}), DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}))\\r\\n| where Effect_s in ({Effect}) and ComplianceState_s in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or SubscriptionGuid_g in ({Subscription}))\\r\\n| where ComplianceState_s !in ('Compliant','Excluded')\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| extend GroupByTag = iif('{FRTagName:label}' == '<unset>' or isempty(parse_json(Tags_s)['{FRTagName:label}']), '', tostring(parse_json(Tags_s)['{FRTagName:label}']))\\r\\n| distinct ResourceId, SubscriptionGuid_g, DefinitionName_s, InitiativeName_s, AssignmentName_s, Effect_s, ComplianceState_s, ComplianceReason_s, GroupByTag;\\r\\nAzureOptimizationPolicyStatesV1_CL\\r\\n| where TimeGenerated > todatetime('{LastPolicyGeneratedDateTime}')-12h\\r\\n| where (\\\"{Initiative}\\\" == \\\"'*'\\\" or InitiativeId_s in ({Initiative})) and DefinitionId_s in ({Definition}) and AssignmentId_s in ({Assignment}) and Effect_s in ({Effect}) and ComplianceState_s in ({ComplianceState}) and (\\\"{Subscription}\\\" == \\\"'*'\\\" or SubscriptionGuid_g in ({Subscription}))\\r\\n| where ComplianceState_s == 'Compliant'\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| project SubscriptionGuid_g, InitiativeName_s, AssignmentName_s, DefinitionName_s, Effect_s, ComplianceState_s, StatesCount=tolong(StatesCount_s), GroupByTag=''\\r\\n| union (AllButCompliantOrExcludedStates)\\r\\n| union (ExcludedStates)\\r\\n| join kind=leftouter ( AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > todatetime('{LastSubsGeneratedDateTime}')-12h\\r\\n    | where ContainerType_s =~ 'microsoft.resources/subscriptions' \\r\\n    | project SubscriptionGuid_g, SubscriptionName = ContainerName_s\\r\\n) on SubscriptionGuid_g\\r\\n| project-away SubscriptionGuid_g*\\r\\n| order by InitiativeName_s asc, DefinitionName_s asc, SubscriptionName asc\\r\\n| extend ResourceId = iif(ResourceId has 'policyinsights', substring(ResourceId, 0, indexof(ResourceId, '/providers/microsoft.policyinsights/')), ResourceId)\\r\\n| extend ResourceCount = iif(isnotempty(ResourceCount), ResourceCount, iif(isnotempty(StatesCount), StatesCount, 1))\\r\\n| distinct ResourceId, GroupByTag, SubscriptionName, DefinitionName_s, InitiativeName_s, AssignmentName_s, Effect_s, ComplianceState_s, ComplianceReason_s, ResourceCount\\r\\n| order by InitiativeName_s asc\",\n              \"size\": 2,\n              \"showAnalytics\": true,\n              \"title\": \"Policy Compliance Full Report\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"DefinitionName_s\",\n                  \"parameterName\": \"selectedDefinition\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"InitiativeName_s\",\n                  \"parameterName\": \"selectedInitiative\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"AssignmentName_s\",\n                  \"parameterName\": \"selectedAssignment\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"SubscriptionName\",\n                  \"parameterName\": \"selectedSubscription\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"ComplianceState_s\",\n                  \"parameterName\": \"selectedState\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"table\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ResourceId\",\n                    \"formatter\": 13,\n                    \"formatOptions\": {\n                      \"linkTarget\": null,\n                      \"showIcon\": true,\n                      \"customColumnWidthSetting\": \"28ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionName\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"DefinitionName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"InitiativeName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AssignmentName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Effect_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ComplianceState_s\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Compliant\",\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"NonCompliant\",\n                          \"representation\": \"error\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Exempt\",\n                          \"representation\": \"info\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ComplianceReason_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"19ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionGuid_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 10000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource\"\n                  },\n                  {\n                    \"columnId\": \"GroupByTag\",\n                    \"label\": \"Group By Tag\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"DefinitionName_s\",\n                    \"label\": \"Definition\"\n                  },\n                  {\n                    \"columnId\": \"InitiativeName_s\",\n                    \"label\": \"Initiative\"\n                  },\n                  {\n                    \"columnId\": \"AssignmentName_s\",\n                    \"label\": \"Assignment\"\n                  },\n                  {\n                    \"columnId\": \"Effect_s\",\n                    \"label\": \"Effect\"\n                  },\n                  {\n                    \"columnId\": \"ComplianceState_s\",\n                    \"label\": \"Compliance\"\n                  },\n                  {\n                    \"columnId\": \"ComplianceReason_s\",\n                    \"label\": \"Reason\"\n                  },\n                  {\n                    \"columnId\": \"ResourceCount\",\n                    \"label\": \"Resources #\"\n                  }\n                ]\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"FullReport\"\n            },\n            \"name\": \"fullReport\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"FullReport\"\n      },\n      \"name\": \"fullReportGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/recommendations.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Recommendations'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '5b6ec066-e5a8-463e-a319-919c0a7d7bb6'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('recommendations.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/recommendations.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"f0273bce-7653-4934-92bf-9f1d89832330\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LastRecommendationDateTime\",\n            \"label\": \"Generated On\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"query\": \"AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > ago(90d)\\r\\n| summarize max(TimeGenerated)\",\n            \"isHiddenWhenLocked\": true,\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          },\n          {\n            \"id\": \"36de52f8-3a82-4ec5-b55a-38f3d5ac2a87\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Subscriptions\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| where isnotempty(SubscriptionName_s)\\r\\n| distinct SubscriptionName_s\\r\\n| order by SubscriptionName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"selectAllValue\": \"*\",\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"8c645a8b-ff34-4402-b064-48ef7f5d14ad\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Impact\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ]\n            },\n            \"jsonData\": \"[\\\"High\\\",\\\"Medium\\\",\\\"Low\\\"]\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Category\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[\\\"Cost\\\", \\\"Security\\\", \\\"OperationalExcellence\\\", \\\"HighAvailability\\\", \\\"Performance\\\"]\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"id\": \"be856cc3-5d47-4c41-a368-4ac80def811b\"\n          },\n          {\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"TagName\",\n            \"label\": \"Tag Name\",\n            \"type\": 2,\n            \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where isnotempty(Tags_s)\\r\\n| extend jsonTags = parse_json(Tags_s)\\r\\n| extend tagKeys = bag_keys(jsonTags)\\r\\n| mv-expand tagKey = tagKeys\\r\\n| extend tagKey = trim(' ', tostring(tagKey))\\r\\n| where tagKey !startswith 'hidden' and tagKey !startswith \\\"aks-managed\\\" and tagKey !startswith \\\"kubernetes.io\\\"\\r\\n| distinct tagKey\\r\\n| order by tagKey asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": null,\n            \"id\": \"cb207140-200f-4237-a00a-f93f61b1ea8e\"\n          },\n          {\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"TagValue\",\n            \"label\": \"Tag Value\",\n            \"type\": 2,\n            \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where isnotempty(Tags_s) and isnotempty(parse_json(Tags_s)['{TagName:label}'])\\r\\n| extend tagValue = tostring(parse_json(Tags_s)['{TagName:label}'])\\r\\n| distinct tagValue\\r\\n| order by tagValue asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": null,\n            \"id\": \"5c4ee4a3-a1a9-4210-8281-888dd20e5394\"\n          }\n        ],\n        \"style\": \"above\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"globalParameters\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"# Latest Recommendations\\r\\nGenerated on **{LastRecommendationDateTime}**. If recommendations generation date is *unset*, review the latest Automation Account runbooks status and fix accordingly.\"\n      },\n      \"name\": \"titleText\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"550ca300-c6e8-4d5e-a4bf-ce30d5032dd5\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Overview\",\n            \"subTarget\": \"Overview\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"e38a5bd0-19c3-48ae-b299-d1eebe76f02a\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Cost\",\n            \"subTarget\": \"Cost\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"27c86ef8-c64d-43be-af08-dd73bd44c0bc\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Security\",\n            \"subTarget\": \"Security\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"1636acc7-3a5c-4553-8a58-16636461d65e\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Operational Excellence\",\n            \"subTarget\": \"OperationalExcellence\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"5c94d48d-e48b-443b-b9d6-76cc1d2c4b51\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"High Availability\",\n            \"subTarget\": \"HighAvailability\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"12182804-33fe-48a3-aca2-7aaeb2367d23\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Performance\",\n            \"subTarget\": \"Performance\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"links - 4\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by Category\\r\\n\",\n              \"size\": 4,\n              \"title\": \"Count by category\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"HighAvailability\",\n                    \"color\": \"lightBlue\"\n                  },\n                  {\n                    \"seriesName\": \"OperationalExcellence\",\n                    \"color\": \"magenta\"\n                  },\n                  {\n                    \"seriesName\": \"Cost\",\n                    \"color\": \"turquoise\"\n                  },\n                  {\n                    \"seriesName\": \"Security\",\n                    \"color\": \"brown\"\n                  },\n                  {\n                    \"seriesName\": \"Performance\",\n                    \"color\": \"blueDarkDark\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Overview\"\n            },\n            \"showPin\": true,\n            \"name\": \"categoriesSummary\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by Impact_s\\r\\n\",\n              \"size\": 4,\n              \"title\": \"Count by impact\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Medium\",\n                    \"color\": \"yellow\"\n                  },\n                  {\n                    \"seriesName\": \"Low\",\n                    \"color\": \"blue\"\n                  },\n                  {\n                    \"seriesName\": \"High\",\n                    \"color\": \"redBright\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Overview\"\n            },\n            \"showPin\": true,\n            \"name\": \"impactSummary\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by SubscriptionName_s\\r\\n\",\n              \"size\": 4,\n              \"title\": \"Count by subscription\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Overview\"\n            },\n            \"showPin\": true,\n            \"name\": \"subscriptionsSummary\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by ImpactedArea_s\",\n              \"size\": 4,\n              \"title\": \"Count by impacted area\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Overview\"\n            },\n            \"showPin\": true,\n            \"name\": \"impactedAreaSummary\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > ago(730d)\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| extend Weight = iif(Impact_s == 'Low', 1, iif(Impact_s == 'Medium', 5, 15))\\r\\n| summarize WeightedCount=sum(Weight) by Category, bin(TimeGenerated, 7d)\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(730d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or ContainerName_s in ({Subscriptions})\\r\\n    | summarize ResourceCount=sum(toint(ResourceCount_s)) by bin(TimeGenerated, 1d)\\r\\n) on TimeGenerated\\r\\n| extend Score = round((1-todouble(WeightedCount)/todouble(ResourceCount))*100,1)\\r\\n| project Category, TimeGenerated, Score\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Optimization Score by Category Over Time (weighted by recommendation impact and overall resources count)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"timechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Cost\",\n                    \"color\": \"turquoise\"\n                  },\n                  {\n                    \"seriesName\": \"OperationalExcellence\",\n                    \"color\": \"magenta\"\n                  },\n                  {\n                    \"seriesName\": \"HighAvailability\",\n                    \"color\": \"lightBlue\"\n                  },\n                  {\n                    \"seriesName\": \"Security\",\n                    \"color\": \"brown\"\n                  },\n                  {\n                    \"seriesName\": \"Performance\",\n                    \"color\": \"blueDarkDark\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"80\",\n            \"name\": \"scoreByCategoryOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > todatetime('{LastRecommendationDateTime}')-27d\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| make-series RecCounts=count() on TimeGenerated from todatetime('{LastRecommendationDateTime}')-27d to todatetime('{LastRecommendationDateTime}') step 7d by InstanceId_s, RecommendationDescription_s\\r\\n| where array_length(RecCounts) == 4 and not(set_has_element(RecCounts, 0))\\r\\n| summarize count() by Text='over past 4 weeks'\",\n              \"size\": 4,\n              \"title\": \"Long standing recommendations #\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"tiles\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"count_\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"none\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 0,\n                    \"options\": {\n                      \"style\": \"decimal\"\n                    }\n                  }\n                },\n                \"subtitleContent\": {\n                  \"columnMatch\": \"Text\",\n                  \"formatter\": 1\n                },\n                \"rightContent\": {\n                  \"columnMatch\": \"Count\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"none\"\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"20\",\n            \"name\": \"longStandingRecs\",\n            \"styleSettings\": {\n              \"margin\": \"20px\",\n              \"padding\": \"20px\"\n            }\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > ago(730d)\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize count() by Category, bin(TimeGenerated, 7d)\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Recommendations # by Category Over Time\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"timechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"OperationalExcellence\",\n                    \"color\": \"magenta\"\n                  },\n                  {\n                    \"seriesName\": \"HighAvailability\",\n                    \"color\": \"lightBlue\"\n                  },\n                  {\n                    \"seriesName\": \"Cost\",\n                    \"color\": \"turquoise\"\n                  },\n                  {\n                    \"seriesName\": \"Security\",\n                    \"color\": \"brown\"\n                  },\n                  {\n                    \"seriesName\": \"Performance\",\n                    \"color\": \"blueDarkDark\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"80\",\n            \"name\": \"recsCategoryOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let PreviousWeek = AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > todatetime('{LastRecommendationDateTime}')-8d and TimeGenerated < todatetime('{LastRecommendationDateTime}')-1d\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize PreviousWeekCount=count() by InstanceId_s, RecommendationSubTypeId_g;\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > todatetime('{LastRecommendationDateTime}')-1d\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize ThisWeekCount=count() by InstanceId_s, RecommendationSubTypeId_g\\r\\n| join kind=leftouter (PreviousWeek) on InstanceId_s, RecommendationSubTypeId_g\\r\\n| where isempty(InstanceId_s1) and isempty(RecommendationSubTypeId_g1)\\r\\n| summarize sum(ThisWeekCount) by Text='since previous week'\",\n              \"size\": 4,\n              \"title\": \"New recommendations #\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"tiles\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"sum_ThisWeekCount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"none\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 0,\n                    \"options\": {\n                      \"style\": \"decimal\"\n                    }\n                  }\n                },\n                \"subtitleContent\": {\n                  \"columnMatch\": \"Text\",\n                  \"formatter\": 1\n                },\n                \"rightContent\": {\n                  \"columnMatch\": \"Count\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"none\"\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"20\",\n            \"name\": \"newRecs\",\n            \"styleSettings\": {\n              \"margin\": \"20px\",\n              \"padding\": \"20px\"\n            }\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > ago(730d)\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize count() by Impact_s, bin(TimeGenerated, 7d)\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Recommendations # by Impact Over Time\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"timechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Medium\",\n                    \"color\": \"yellow\"\n                  },\n                  {\n                    \"seriesName\": \"High\",\n                    \"color\": \"redBright\"\n                  },\n                  {\n                    \"seriesName\": \"Low\",\n                    \"color\": \"blue\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"80\",\n            \"name\": \"recsImpactOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let PreviousWeek = AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > todatetime('{LastRecommendationDateTime}')-8d and TimeGenerated < todatetime('{LastRecommendationDateTime}')-1d\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize PreviousWeekCount=count() by InstanceId_s, RecommendationSubTypeId_g;\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > todatetime('{LastRecommendationDateTime}')-1d\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize ThisWeekCount=count() by InstanceId_s, RecommendationSubTypeId_g\\r\\n| join kind=rightouter (PreviousWeek) on InstanceId_s, RecommendationSubTypeId_g\\r\\n| where isempty(InstanceId_s) and isempty(RecommendationSubTypeId_g)\\r\\n| summarize sum(PreviousWeekCount) by Text='since previous week'\",\n              \"size\": 4,\n              \"title\": \"Dropped recommendations #\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"tiles\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"sum_PreviousWeekCount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"none\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 0,\n                    \"options\": {\n                      \"style\": \"decimal\"\n                    }\n                  }\n                },\n                \"subtitleContent\": {\n                  \"columnMatch\": \"Text\",\n                  \"formatter\": 1\n                },\n                \"rightContent\": {\n                  \"columnMatch\": \"Count\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"none\"\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"20\",\n            \"name\": \"droppedRecs\",\n            \"styleSettings\": {\n              \"margin\": \"20px\",\n              \"padding\": \"20px\"\n            }\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > ago(730d)\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where Category in ({Category})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| summarize RecCount=count() by bin(TimeGenerated, 7d)\\r\\n| join kind=inner ( \\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(730d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or ContainerName_s in ({Subscriptions})\\r\\n    | summarize ResourceCount=sum(toint(ResourceCount_s)) by bin(TimeGenerated, 1d)\\r\\n) on TimeGenerated\\r\\n| project TimeGenerated, ResourceCount\",\n              \"size\": 1,\n              \"aggregation\": 3,\n              \"title\": \"Resources # Over Time\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"timechart\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"Cost\",\n                    \"color\": \"turquoise\"\n                  },\n                  {\n                    \"seriesName\": \"OperationalExcellence\",\n                    \"color\": \"magenta\"\n                  },\n                  {\n                    \"seriesName\": \"HighAvailability\",\n                    \"color\": \"lightBlue\"\n                  },\n                  {\n                    \"seriesName\": \"Security\",\n                    \"color\": \"brown\"\n                  },\n                  {\n                    \"seriesName\": \"Performance\",\n                    \"color\": \"blueDarkDark\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"80\",\n            \"name\": \"resourcesOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Overview\"\n      },\n      \"name\": \"overviewGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"* Estimated cost savings are relative to 30 days of consumption.\\r\\n* Cost values are based on the currency of your consumption agreement. Adjust the currency parameter below to reflect your actual currency (changing the currency code does not perform any currency exchange in cost values).\",\n              \"style\": \"info\"\n            },\n            \"name\": \"text - 5\"\n          },\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"143959c5-1b91-4f61-bc65-970aa2c66b8d\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Currency\",\n                  \"type\": 1,\n                  \"description\": \"The currency to display savings amounts. Must be your Azure consumption agreement currency.\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"EUR\"\n                },\n                {\n                  \"id\": \"1d6e5f51-df51-4989-90ee-0a014253bbf9\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"USDRate\",\n                  \"label\": \"USD Rate\",\n                  \"type\": 1,\n                  \"description\": \"Used to convert some of the Azure Advisor-generated USD savings into your currency. If your currency is USD, set the rate to 1.\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"0.93\"\n                },\n                {\n                  \"id\": \"941690c0-8e7f-48fe-b8ad-f668b066bae5\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"CostFitScore\",\n                  \"label\": \"Min. Fit Score\",\n                  \"type\": 1,\n                  \"description\": \"Minimum acceptable fit score\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"-1\"\n                },\n                {\n                  \"id\": \"69ecab4f-05bb-4b53-aa39-68a30e09a70b\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Origin\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ]\n                  },\n                  \"jsonData\": \"[\\\"Advisor\\\",\\\"Custom\\\"]\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"id\": \"57abf104-8b39-4d00-934b-1365a8d34266\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"RecommendationsSubType\",\n                  \"label\": \"Type\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Cost'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| distinct RecommendationSubType_s\\r\\n| order by RecommendationSubType_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"CostRecommendations\",\n                  \"label\": \"Recommendations\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Cost'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| where RecommendationSubType_s in ({RecommendationsSubType:value})\\r\\n| distinct RecommendationDescription_s\\r\\n| order by RecommendationDescription_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"id\": \"64fb1183-a7fa-43e7-be95-cc7a3afc4733\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"costParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Cost'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {CostFitScore}\\r\\n| where RecommendationDescription_s in ({CostRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\\r\\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\\r\\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\\r\\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\\r\\n| summarize sum(SavingsAmount) by SubscriptionName_s\",\n              \"size\": 4,\n              \"title\": \"Potential cost savings by subscription (in {Currency})\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Cost\"\n            },\n            \"showPin\": true,\n            \"name\": \"costSavingsBySubscription\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Cost'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {CostFitScore}\\r\\n| where RecommendationDescription_s in ({CostRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\\r\\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\\r\\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\\r\\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\\r\\n| summarize sum(SavingsAmount) by RecommendationSubType_s\",\n              \"size\": 4,\n              \"title\": \"Potential savings by type (in {Currency})\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Cost\"\n            },\n            \"showPin\": true,\n            \"name\": \"costSavingsByType\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Cost'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {CostFitScore}\\r\\n| where RecommendationDescription_s in ({CostRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\\r\\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\\r\\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\\r\\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| summarize sum(SavingsAmount) by Origin\",\n              \"size\": 4,\n              \"title\": \"Potential savings by origin (in {Currency})\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Cost\"\n            },\n            \"showPin\": true,\n            \"name\": \"costSavingsByOrigin\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| where Category == 'Cost'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {CostFitScore}\\r\\n| where RecommendationDescription_s in ({CostRecommendations})\\r\\n| extend SavingsAmount = todouble(parse_json(AdditionalInfo_s).savingsAmount)\\r\\n| summarize arg_max(SavingsAmount, *) by RecommendationSubTypeId_g, InstanceName_s, ResourceGroup, SubscriptionGuid_g, tostring(parse_json(AdditionalInfo_s).sku)\\r\\n| extend Currency = iif(isnotempty(parse_json(AdditionalInfo_s).savingsCurrency), parse_json(AdditionalInfo_s).savingsCurrency, '{Currency}')\\r\\n| extend SavingsAmount = iif(Currency == 'USD', SavingsAmount*{USDRate}, SavingsAmount)\\r\\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, ['Savings ({Currency})']=SavingsAmount, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\\r\\n| order by ['Savings ({Currency})']\",\n              \"size\": 0,\n              \"noDataMessage\": \"No Cost recommendations available\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Recommendation\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Instance\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Resource Group\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Subscription\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Impact\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"High\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Medium\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Low\",\n                          \"representation\": \"Sev3\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Fit Score\",\n                    \"formatter\": 8,\n                    \"formatOptions\": {\n                      \"min\": 0,\n                      \"max\": 5,\n                      \"palette\": \"redGreen\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Savings\",\n                    \"formatter\": 2,\n                    \"formatOptions\": {\n                      \"aggregation\": \"Sum\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Tags\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Additional Info\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Details\",\n                    \"formatter\": 7,\n                    \"formatOptions\": {\n                      \"linkTarget\": \"Url\",\n                      \"linkLabel\": \"Link\"\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Cost\"\n            },\n            \"name\": \"costRecommendationsList\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Cost\"\n      },\n      \"name\": \"costGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"941690c0-8e7f-48fe-b8ad-f668b066bae5\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"SecurityFitScore\",\n                  \"label\": \"Min. Fit Score\",\n                  \"type\": 1,\n                  \"description\": \"Minimum acceptable fit score\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"-1\"\n                },\n                {\n                  \"id\": \"404387de-475b-4729-b0d9-b6acf8d9594e\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Origin\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ]\n                  },\n                  \"jsonData\": \"[\\\"Advisor\\\",\\\"Custom\\\"]\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"id\": \"57abf104-8b39-4d00-934b-1365a8d34266\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"RecommendationsSubType\",\n                  \"label\": \"Type\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Security'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| distinct RecommendationSubType_s\\r\\n| order by RecommendationSubType_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"SecurityRecommendations\",\n                  \"label\": \"Recommendations\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Security'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| where RecommendationSubType_s in ({RecommendationsSubType:value})\\r\\n| distinct RecommendationDescription_s\\r\\n| order by RecommendationDescription_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"id\": \"42b5d3ed-d221-496e-b911-b91c0ca73983\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"costParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Security'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {SecurityFitScore}\\r\\n| where RecommendationDescription_s in ({SecurityRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by SubscriptionName_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by subscription\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Security\"\n            },\n            \"showPin\": true,\n            \"name\": \"securityRecsBySubscription\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Security'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {SecurityFitScore}\\r\\n| where RecommendationDescription_s in ({SecurityRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by RecommendationSubType_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by type\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Security\"\n            },\n            \"showPin\": true,\n            \"name\": \"securityRecommendationsByType\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Security'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {SecurityFitScore}\\r\\n| where RecommendationDescription_s in ({SecurityRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| summarize count() by Origin\",\n              \"size\": 4,\n              \"title\": \"Recommendations by origin\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Security\"\n            },\n            \"showPin\": true,\n            \"name\": \"securityRecommendationsByOrigin\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| where Category == 'Security'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {SecurityFitScore}\\r\\n| where RecommendationDescription_s in ({SecurityRecommendations})\\r\\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\\r\\n| order by Impact asc, ['Fit Score']\",\n              \"size\": 2,\n              \"noDataMessage\": \"No Security recommendations available\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Recommendation\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Instance\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Resource Group\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Subscription\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Impact\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"High\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Medium\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Low\",\n                          \"representation\": \"Sev3\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Fit Score\",\n                    \"formatter\": 8,\n                    \"formatOptions\": {\n                      \"min\": 0,\n                      \"max\": 5,\n                      \"palette\": \"redGreen\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Tags\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Additional Info\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Details\",\n                    \"formatter\": 7,\n                    \"formatOptions\": {\n                      \"linkTarget\": \"Url\",\n                      \"linkLabel\": \"Link\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Savings\",\n                    \"formatter\": 2,\n                    \"formatOptions\": {\n                      \"aggregation\": \"Sum\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Security\"\n            },\n            \"name\": \"securityRecommendationsList\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Security\"\n      },\n      \"name\": \"securityGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"941690c0-8e7f-48fe-b8ad-f668b066bae5\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"OperationalExcellenceFitScore\",\n                  \"label\": \"Min. Fit Score\",\n                  \"type\": 1,\n                  \"description\": \"Minimum acceptable fit score\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"-1\"\n                },\n                {\n                  \"id\": \"88e40f64-4813-4766-a0bc-0e067dbfceb9\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Origin\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ]\n                  },\n                  \"jsonData\": \"[\\\"Advisor\\\",\\\"Custom\\\"]\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"id\": \"57abf104-8b39-4d00-934b-1365a8d34266\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"RecommendationsSubType\",\n                  \"label\": \"Type\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'OperationalExcellence'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| distinct RecommendationSubType_s\\r\\n| order by RecommendationSubType_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"OperationalExcellenceRecommendations\",\n                  \"label\": \"Recommendations\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'OperationalExcellence'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| where RecommendationSubType_s in ({RecommendationsSubType:value})\\r\\n| distinct RecommendationDescription_s\\r\\n| order by RecommendationDescription_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"id\": \"2f82e306-088a-417f-b896-0fa126974807\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"operationalExcellenceParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'OperationalExcellence'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {OperationalExcellenceFitScore}\\r\\n| where RecommendationDescription_s in ({OperationalExcellenceRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by SubscriptionName_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by subscription\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"OperationalExcellence\"\n            },\n            \"showPin\": true,\n            \"name\": \"operationalExcellenceRecsBySubscription\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'OperationalExcellence'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {OperationalExcellenceFitScore}\\r\\n| where RecommendationDescription_s in ({OperationalExcellenceRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by RecommendationSubType_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by type\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"OperationalExcellence\"\n            },\n            \"showPin\": true,\n            \"name\": \"operationalExcellenceRecommendationsByType\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'OperationalExcellence'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {OperationalExcellenceFitScore}\\r\\n| where RecommendationDescription_s in ({OperationalExcellenceRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| summarize count() by Origin\",\n              \"size\": 4,\n              \"title\": \"Recommendations by origin\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"OperationalExcellence\"\n            },\n            \"showPin\": true,\n            \"name\": \"operationalExcellenceRecommendationsByOrigin\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| where Category == 'OperationalExcellence'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {OperationalExcellenceFitScore}\\r\\n| where RecommendationDescription_s in ({OperationalExcellenceRecommendations})\\r\\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\\r\\n| order by Impact asc, ['Fit Score']\",\n              \"size\": 2,\n              \"noDataMessage\": \"No Operational Excellence recommendations available\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Recommendation\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Instance\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Resource Group\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Subscription\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Impact\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"High\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Medium\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Low\",\n                          \"representation\": \"Sev3\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Fit Score\",\n                    \"formatter\": 8,\n                    \"formatOptions\": {\n                      \"min\": 0,\n                      \"max\": 5,\n                      \"palette\": \"redGreen\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Tags\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Additional Info\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Details\",\n                    \"formatter\": 7,\n                    \"formatOptions\": {\n                      \"linkTarget\": \"Url\",\n                      \"linkLabel\": \"Link\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Savings\",\n                    \"formatter\": 2,\n                    \"formatOptions\": {\n                      \"aggregation\": \"Sum\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"OperationalExcellence\"\n            },\n            \"name\": \"operationalExcellenceRecommendationsList\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"OperationalExcellence\"\n      },\n      \"name\": \"operationalExcellenceGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"941690c0-8e7f-48fe-b8ad-f668b066bae5\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"HighAvailabilityFitScore\",\n                  \"label\": \"Min. Fit Score\",\n                  \"type\": 1,\n                  \"description\": \"Minimum acceptable fit score\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"-1\"\n                },\n                {\n                  \"id\": \"c5da94b9-1eaa-4a1a-aa0f-62733fb6a3cc\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Origin\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"jsonData\": \"[\\\"Advisor\\\",\\\"Custom\\\"]\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"id\": \"57abf104-8b39-4d00-934b-1365a8d34266\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"RecommendationsSubType\",\n                  \"label\": \"Type\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'HighAvailability'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| distinct RecommendationSubType_s\\r\\n| order by RecommendationSubType_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"HighAvailabilityRecommendations\",\n                  \"label\": \"Recommendations\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'HighAvailability'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| where RecommendationSubType_s in ({RecommendationsSubType:value})\\r\\n| distinct RecommendationDescription_s\\r\\n| order by RecommendationDescription_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"id\": \"06f783a2-135e-4906-a8b6-cbb6eedfb919\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"highAvailabilityParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'HighAvailability'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {HighAvailabilityFitScore}\\r\\n| where RecommendationDescription_s in ({HighAvailabilityRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by SubscriptionName_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by subscription\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"HighAvailability\"\n            },\n            \"showPin\": true,\n            \"name\": \"highAvailabilityRecsBySubscription\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'HighAvailability'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {HighAvailabilityFitScore}\\r\\n| where RecommendationDescription_s in ({HighAvailabilityRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by RecommendationSubType_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by type\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"HighAvailability\"\n            },\n            \"showPin\": true,\n            \"name\": \"highAvailabilityRecommendationsByType\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'HighAvailability'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {HighAvailabilityFitScore}\\r\\n| where RecommendationDescription_s in ({HighAvailabilityRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| summarize count() by Origin\",\n              \"size\": 4,\n              \"title\": \"Recommendations by origin\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"HighAvailability\"\n            },\n            \"showPin\": true,\n            \"name\": \"highAvailabilityRecommendationsByOrigin\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| where Category == 'HighAvailability'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {HighAvailabilityFitScore}\\r\\n| where RecommendationDescription_s in ({HighAvailabilityRecommendations})\\r\\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\\r\\n| order by Impact asc, ['Fit Score']\",\n              \"size\": 2,\n              \"noDataMessage\": \"No High Availability recommendations available\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Recommendation\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Instance\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Resource Group\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Subscription\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Impact\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"High\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Medium\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Low\",\n                          \"representation\": \"Sev3\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Fit Score\",\n                    \"formatter\": 8,\n                    \"formatOptions\": {\n                      \"min\": 0,\n                      \"max\": 5,\n                      \"palette\": \"redGreen\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Tags\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Additional Info\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Details\",\n                    \"formatter\": 7,\n                    \"formatOptions\": {\n                      \"linkTarget\": \"Url\",\n                      \"linkLabel\": \"Link\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Savings\",\n                    \"formatter\": 2,\n                    \"formatOptions\": {\n                      \"aggregation\": \"Sum\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"HighAvailability\"\n            },\n            \"name\": \"highAvailabilityRecommendationsList\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"HighAvailability\"\n      },\n      \"name\": \"highAvailabilityGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"941690c0-8e7f-48fe-b8ad-f668b066bae5\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"PerformanceFitScore\",\n                  \"label\": \"Min. Fit Score\",\n                  \"type\": 1,\n                  \"description\": \"Minimum acceptable fit score\",\n                  \"isRequired\": true,\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"value\": \"-1\"\n                },\n                {\n                  \"id\": \"74975b92-2f5d-4e1f-81ec-7af18fe851e6\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"Origin\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ]\n                  },\n                  \"jsonData\": \"[\\\"Advisor\\\",\\\"Custom\\\"]\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"id\": \"57abf104-8b39-4d00-934b-1365a8d34266\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"RecommendationsSubType\",\n                  \"label\": \"Type\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Performance'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| distinct RecommendationSubType_s\\r\\n| order by RecommendationSubType_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ]\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"PerformanceRecommendations\",\n                  \"label\": \"Recommendations\",\n                  \"type\": 2,\n                  \"isRequired\": true,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Performance'\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| where Origin in ({Origin:value})\\r\\n| where RecommendationSubType_s in ({RecommendationsSubType:value})\\r\\n| distinct RecommendationDescription_s\\r\\n| order by RecommendationDescription_s asc\",\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"showDefault\": false\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"id\": \"3e1c7c88-d638-41d2-88ca-9f237eb2dc95\"\n                }\n              ],\n              \"style\": \"above\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"name\": \"performanceParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Performance'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {PerformanceFitScore}\\r\\n| where RecommendationDescription_s in ({PerformanceRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by SubscriptionName_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by subscription\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Performance\"\n            },\n            \"showPin\": true,\n            \"name\": \"performanceRecsBySubscription\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Performance'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {PerformanceFitScore}\\r\\n| where RecommendationDescription_s in ({PerformanceRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| summarize count() by RecommendationSubType_s\",\n              \"size\": 4,\n              \"title\": \"Recommendations by type\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Performance\"\n            },\n            \"showPin\": true,\n            \"name\": \"performanceRecommendationsByType\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| where Category == 'Performance'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {PerformanceFitScore}\\r\\n| where RecommendationDescription_s in ({PerformanceRecommendations})\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| extend Origin = iif(RecommendationSubType_s startswith \\\"Advisor\\\", \\\"Advisor\\\", \\\"Custom\\\")\\r\\n| summarize count() by Origin\",\n              \"size\": 4,\n              \"title\": \"Recommendations by origin\",\n              \"noDataMessage\": \"No recommendations available. Review the latest Automation Account runbooks status and fix accordingly.\",\n              \"noDataMessageStyle\": 4,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"piechart\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"Category\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"sum_SavingsAmount\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 17,\n                    \"options\": {\n                      \"style\": \"decimal\",\n                      \"maximumFractionDigits\": 2,\n                      \"maximumSignificantDigits\": 3\n                    }\n                  }\n                },\n                \"showBorder\": false\n              },\n              \"textSettings\": {\n                \"style\": \"bignumber\"\n              }\n            },\n            \"customWidth\": \"33\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Performance\"\n            },\n            \"showPin\": true,\n            \"name\": \"performanceRecommendationsByOrigin\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let beginTime = todatetime('{LastRecommendationDateTime}')-6d;\\r\\nlet ActiveGlobalSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isempty(InstanceId_g) and isempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g;\\r\\nlet ActiveSubscriptionSuppressions = AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceId_g)\\r\\n| extend SubscriptionGuid_g = InstanceId_g\\r\\n| project RecommendationSubTypeId_g, SubscriptionGuid_g;\\r\\nlet ActiveInstanceSuppressions = materialize(AzureOptimizationSuppressionsV1_CL \\r\\n| where TimeGenerated > beginTime and FilterStartDate_t < beginTime and FilterEndDate_t > now()\\r\\n| where FilterType_s in ('Snooze','Dismiss')\\r\\n| where isnotempty(InstanceName_s)\\r\\n| project RecommendationSubTypeId_g, InstanceName_s);\\r\\nAzureOptimizationRecommendationsV1_CL\\r\\n| where TimeGenerated > beginTime\\r\\n| join kind=leftouter ( ActiveGlobalSuppressions ) on RecommendationSubTypeId_g\\r\\n| where isempty(RecommendationSubTypeId_g1)\\r\\n| join kind=leftouter ( ActiveSubscriptionSuppressions ) on RecommendationSubTypeId_g and SubscriptionGuid_g\\r\\n| where isempty(RecommendationSubTypeId_g2)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.InstanceName_s == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g3)\\r\\n| join kind=leftouter ( ActiveInstanceSuppressions ) on RecommendationSubTypeId_g and $left.ResourceGroup == $right.InstanceName_s\\r\\n| where isempty(RecommendationSubTypeId_g4)\\r\\n| where Category == 'Performance'\\r\\n| where \\\"'*'\\\" == \\\"{Subscriptions}\\\" or SubscriptionName_s in ({Subscriptions})\\r\\n| where Impact_s in ({Impact})\\r\\n| where iif('{TagName:label}' == '<unset>' or '{TagValue:label}' == '<unset>', true, isnotempty(parse_json(Tags_s)['{TagName:label}']) and tostring(parse_json(Tags_s)['{TagName:label}']) =~ '{TagValue}')\\r\\n| where FitScore_d >= {PerformanceFitScore}\\r\\n| where RecommendationDescription_s in ({PerformanceRecommendations})\\r\\n| project Recommendation=RecommendationDescription_s, Instance=InstanceName_s, ['Resource Group']=ResourceGroup, Subscription=SubscriptionName_s, Impact=Impact_s, ['Fit Score']=FitScore_d, Tags=parse_json(Tags_s), ['Additional Info']=parse_json(AdditionalInfo_s), Details=DetailsURL_s\\r\\n| order by Impact asc, ['Fit Score']\",\n              \"size\": 2,\n              \"noDataMessage\": \"No Performance recommendations available\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Recommendation\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Instance\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Resource Group\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Subscription\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Impact\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"High\",\n                          \"representation\": \"Sev1\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Medium\",\n                          \"representation\": \"Sev2\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Low\",\n                          \"representation\": \"Sev3\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"more\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Fit Score\",\n                    \"formatter\": 8,\n                    \"formatOptions\": {\n                      \"min\": 0,\n                      \"max\": 5,\n                      \"palette\": \"redGreen\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Tags\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Additional Info\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Details\",\n                    \"formatter\": 7,\n                    \"formatOptions\": {\n                      \"linkTarget\": \"Url\",\n                      \"linkLabel\": \"Link\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Savings\",\n                    \"formatter\": 2,\n                    \"formatOptions\": {\n                      \"aggregation\": \"Sum\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"selectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Performance\"\n            },\n            \"name\": \"performanceRecommendationsList\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Performance\"\n      },\n      \"name\": \"performanceGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/reservations-potential.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Reservations Potential'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '14707f9b-03c4-43ff-9811-2b2cc1c74b61'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('reservations-potential.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/reservations-potential.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"b58b4eb8-5821-44d2-bc7e-54054df27320\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LookbackPeriod\",\n            \"label\": \"Lookback Period\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ]\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          },\n          {\n            \"id\": \"3e36a073-14b2-4406-a84a-1b6d0a15f363\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"UseISF\",\n            \"label\": \"Instance Size Flexibility?\",\n            \"type\": 10,\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[\\\"Yes\\\", \\\"No\\\"]\",\n            \"value\": \"No\"\n          },\n          {\n            \"id\": \"367a8fc6-9e49-47cd-af4b-deea6cfcf538\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"AggregatorTag\",\n            \"label\": \"Aggregator Tag\",\n            \"type\": 1,\n            \"description\": \"Tag name for the RI potential by tag value analysis for a specific size/ISF group\",\n            \"value\": null\n          }\n        ],\n        \"style\": \"above\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters - 0\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Consumption data is updated once every 24 hours and is presented in the currency of your Azure consumption agreement.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"info\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).\",\n        \"style\": \"warning\"\n      },\n      \"name\": \"text - 5\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where todatetime(Date_s) > todatetime('{LookbackPeriod:startISO}') and todatetime(Date_s) < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and MeterCategory_s == 'Virtual Machines'\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationResourceContainersV1_CL\\r\\n    | where TimeGenerated > ago(2d)\\r\\n    | where ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | distinct SubscriptionId=SubscriptionGuid_g, SubscriptionName=ContainerName_s, Cloud=Cloud_s\\r\\n) on SubscriptionId\\r\\n| where Cloud == 'AzureCloud'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s)['ServiceType'])\\r\\n| extend SKUName = strcat(VMSize, ' ', ResourceLocation_s)\\r\\n| project Date_s, QtyHours=todouble(Quantity_s), BillingMeter=MeterName_s, ResourceLocation_s, MeterId=MeterId_g, ResourceId, OnDemandPrice=todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s)), SubscriptionName, VMSize, SKUName\\r\\n| join kind=leftouter ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| extend ISFGroup = strcat(ISFGroup, ' ', ResourceLocation_s)\\r\\n| extend OnDemandCost = QtyHours * OnDemandPrice\\r\\n| summarize DailyCost=sum(OnDemandCost) by bin(todatetime(Date_s), 1d), iif(\\\"{UseISF}\\\" == \\\"Yes\\\", ISFGroup, SKUName)\\r\\n| order by DailyCost\",\n        \"size\": 0,\n        \"title\": \"Average on-demand (PAYG) daily consumption (actual cost - Virtual Machines only)\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"barchart\"\n      },\n      \"name\": \"onDemandUsageAsIs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage' and ResourceLocation_s !startswith 'china'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize=tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| where isnotempty(VMSize)\\r\\n| summarize UsedQuantity = sum(todouble(Quantity_s)) by ResourceId, Date_s, VMSize, ResourceLocation_s\\r\\n| summarize RIPotential=sum(UsedQuantity/24), AvgSizeUsageHours=avg(UsedQuantity) by Date_s, VMSize, ResourceLocation_s\\r\\n| summarize RIPotential=round(avg(RIPotential),1), AvgSizeUsageHours=round(avg(AvgSizeUsageHours)) by VMSize, ResourceLocation_s\\r\\n| extend Fragmentation = case(AvgSizeUsageHours >= 24.0, 0.0, AvgSizeUsageHours >= 18.0 and AvgSizeUsageHours < 24.0, 0.25, AvgSizeUsageHours >= 12.0 and AvgSizeUsageHours < 18.0, 0.5, AvgSizeUsageHours >= 6.0 and AvgSizeUsageHours < 12.0, 0.75, 1.0)\\r\\n| project-reorder VMSize, ResourceLocation_s, RIPotential, Fragmentation\\r\\n| order by Fragmentation asc, RIPotential desc\",\n              \"size\": 0,\n              \"title\": \"On-demand sizes usage and RI potential/fragmentation (click on a line for more details)\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"VMSize\",\n                  \"parameterName\": \"VMSize\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"ResourceLocation_s\",\n                  \"parameterName\": \"Location\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"RIPotential\",\n                  \"parameterName\": \"RIPotential\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"Fragmentation\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"colors\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0\",\n                          \"representation\": \"green\",\n                          \"text\": \"low to none\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0.25\",\n                          \"representation\": \"yellow\",\n                          \"text\": \"some\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0.5\",\n                          \"representation\": \"orange\",\n                          \"text\": \"some to high\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0.75\",\n                          \"representation\": \"purple\",\n                          \"text\": \"high\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"1\",\n                          \"representation\": \"red\",\n                          \"text\": \"very high\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"blue\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"tooltipFormat\": {\n                      \"tooltip\": \"On-demand usage has {0} fragmentation across multiple VMs with respect to the average count\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgSizeUsageHours\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 1000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"VMSize\",\n                    \"label\": \"Size\"\n                  },\n                  {\n                    \"columnId\": \"ResourceLocation_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"RIPotential\",\n                    \"label\": \"VMs # (Avg.)\"\n                  },\n                  {\n                    \"columnId\": \"Fragmentation\",\n                    \"label\": \"Fragmentation\"\n                  },\n                  {\n                    \"columnId\": \"AvgSizeUsageHours\",\n                    \"label\": \"Usage (Avg. Hrs.)\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"45\",\n            \"name\": \"riPotential\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend VMSize=tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| where VMSize == '{VMSize}' and ResourceLocation_s =~ '{Location}'\\r\\n| summarize UsedQuantity = round(sum(todouble(Quantity_s)/24)) by todatetime(Date_s)\\r\\n| extend RIPotential = {RIPotential}\\r\\n| render timechart\",\n              \"size\": 0,\n              \"aggregation\": 3,\n              \"title\": \"Instance count for selected size/location (click on a line in the table at the left)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"UsedQuantity\",\n                    \"label\": \"Instance #\"\n                  },\n                  {\n                    \"seriesName\": \"RIPotential\",\n                    \"label\": \"RI Potential\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"55\",\n            \"name\": \"riPotentialOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize=tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| where VMSize == '{VMSize}' and ResourceLocation_s =~ '{Location}'\\r\\n| summarize UsedQuantity = sum(todouble(Quantity_s)) by Date_s, ResourceId, SubscriptionId\\r\\n| summarize AvgUsedQuantity = round(avg(UsedQuantity),1) by ResourceId, SubscriptionId\\r\\n| join kind=leftouter ( AzureOptimizationResourceContainersV1_CL | where TimeGenerated > ago(1d) and ContainerType_s =~ 'microsoft.resources/subscriptions' | project SubscriptionName=ContainerName_s, SubscriptionId=SubscriptionGuid_g) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder ResourceId, SubscriptionName\\r\\n| extend AvgUsedVMs = AvgUsedQuantity / 24\\r\\n| order by ResourceId asc\",\n              \"size\": 1,\n              \"title\": \"Daily on-demand usage for selected size/location by resource (click on a line in the table above)\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"SubscriptionName\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedQuantity\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedVMs\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedQuantity\",\n                    \"label\": \"Avg. hrs\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedVMs\",\n                    \"label\": \"Avg. VMs\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riPotentialInstances\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize=tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| where VMSize == '{VMSize}' and ResourceLocation_s =~ '{Location}'\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{AggregatorTag}'])\\r\\n| summarize UsedQuantity = sum(todouble(Quantity_s)) by Date_s, AggregatorTag, SubscriptionId\\r\\n| summarize AvgUsedQuantity = round(avg(UsedQuantity),1) by AggregatorTag, SubscriptionId\\r\\n| join kind=leftouter ( AzureOptimizationResourceContainersV1_CL | where TimeGenerated > ago(1d) and ContainerType_s =~ 'microsoft.resources/subscriptions' | project SubscriptionName=ContainerName_s, SubscriptionId=SubscriptionGuid_g) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder AggregatorTag, SubscriptionName\\r\\n| extend AvgUsedVMs = AvgUsedQuantity / 24\\r\\n| order by AggregatorTag asc\",\n              \"size\": 1,\n              \"title\": \"Daily on-demand usage for selected size/location by tag (click on a line in the table above)\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"SubscriptionName\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedQuantity\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"25ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedVMs\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedQuantity\",\n                    \"label\": \"Avg. hrs\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedVMs\",\n                    \"label\": \"Avg. VMs\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riPotentialTag\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandPrice;\\r\\nAzureOptimizationReservationsPriceV1_CL\\r\\n| where TimeGenerated > ago(7d)\\r\\n| where armSkuName_s =~ '{VMSize}' and tolower(armRegionName_s) =~ '{Location}'\\r\\n| extend reservationUnitPrice = iif(reservationTerm_s == '1 Year', todouble(unitPrice_s)/12/730, todouble(unitPrice_s)/3/12/730)\\r\\n| project unitPrice_s, reservationUnitPrice, currencyCode_s, reservationTerm_s, armSkuName_s, armRegionName_s\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationConsumptionV1_CL\\r\\n    | where ChargeType_s == 'Usage' and ResourceLocation_s !startswith 'china'\\r\\n    | extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n    | where PricingModel == 'OnDemand'\\r\\n    | extend armSkuName_s=tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n    | extend armRegionName_s = tolower(ResourceLocation_s)\\r\\n    | extend UnitPrice=todouble(iif(todouble(UnitPrice_s) > 0.0, UnitPrice_s, EffectivePrice_s))\\r\\n    | distinct armSkuName_s, armRegionName_s, MeterId_g\\r\\n    | join kind=inner ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n    | summarize OnDemandUnitPrice=max(OnDemandPrice) by armSkuName_s, armRegionName_s\\r\\n) on armRegionName_s, armSkuName_s\\r\\n| extend savingsPercentage =  (1 - reservationUnitPrice / OnDemandUnitPrice) * 100\\r\\n| extend commitmentCost = todouble(unitPrice_s) * bin({RIPotential},1)\\r\\n| extend vmCount = bin({RIPotential},1)\\r\\n| project reservationTerm_s, vmCount, commitmentCost, currencyCode_s, savingsPercentage\",\n              \"size\": 4,\n              \"title\": \"Estimated Commitment and Savings\",\n              \"noDataMessage\": \"No reservations available for this VM size\",\n              \"timeContext\": {\n                \"durationMs\": 604800000\n              },\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"reservationTerm_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"10ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"commitmentCost\",\n                    \"formatter\": 1,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"currencyCode_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"savingsPercentage\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"reservationTerm_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"vmCount\",\n                    \"label\": \"VMs\"\n                  },\n                  {\n                    \"columnId\": \"commitmentCost\",\n                    \"label\": \"Commitment\"\n                  },\n                  {\n                    \"columnId\": \"currencyCode_s\",\n                    \"label\": \"Currency\"\n                  },\n                  {\n                    \"columnId\": \"savingsPercentage\",\n                    \"label\": \"Savings\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"reservationPriceEstimation\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"UseISF\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"No\"\n      },\n      \"name\": \"noISFGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"VMs/instance count for each Instance Size Flexibility Group is proportional to the VM size with the lowest ratio (1).\",\n              \"style\": \"warning\"\n            },\n            \"name\": \"text - 4\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage' and ResourceLocation_s !startswith 'china'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize=tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| where isnotempty(VMSize)\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| summarize UsedQuantity = sum(todouble(Quantity_s)) by ResourceId, Date_s, VMSize, ISFGroup, Ratio, ResourceLocation_s\\r\\n| summarize RIPotential=sum(UsedQuantity/24*Ratio), AvgSizeUsageHours=avg(UsedQuantity) by Date_s, ISFGroup, ResourceLocation_s\\r\\n| summarize RIPotential=round(avg(RIPotential),1), AvgSizeUsageHours=round(avg(AvgSizeUsageHours)) by ISFGroup, ResourceLocation_s\\r\\n| extend Fragmentation = case(AvgSizeUsageHours >= 24.0, 0.0, AvgSizeUsageHours >= 18.0 and AvgSizeUsageHours < 24.0, 0.25, AvgSizeUsageHours >= 12.0 and AvgSizeUsageHours < 18.0, 0.5, AvgSizeUsageHours >= 6.0 and AvgSizeUsageHours < 12.0, 0.75, 1.0)\\r\\n| project-reorder ISFGroup, ResourceLocation_s, RIPotential, Fragmentation\\r\\n| order by Fragmentation asc, RIPotential desc\",\n              \"size\": 0,\n              \"title\": \"On-demand ISF group usage and RI potential/fragmentation (click on a line for more details)\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"ISFGroup\",\n                  \"parameterName\": \"ISFGroup\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"ResourceLocation_s\",\n                  \"parameterName\": \"Location\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"RIPotential\",\n                  \"parameterName\": \"RIPotential\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ISFGroup\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"26ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ResourceLocation_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Fragmentation\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"colors\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0\",\n                          \"representation\": \"green\",\n                          \"text\": \"low to none\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0.25\",\n                          \"representation\": \"yellow\",\n                          \"text\": \"some\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0.5\",\n                          \"representation\": \"orange\",\n                          \"text\": \"some to high\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"0.75\",\n                          \"representation\": \"purple\",\n                          \"text\": \"high\"\n                        },\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"1\",\n                          \"representation\": \"red\",\n                          \"text\": \"very high\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"blue\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"tooltipFormat\": {\n                      \"tooltip\": \"On-demand usage has {0} fragmentation across multiple VMs with respect to the average count\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgSizeUsageHours\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 1000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ISFGroup\",\n                    \"label\": \"ISF Group\"\n                  },\n                  {\n                    \"columnId\": \"ResourceLocation_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"RIPotential\",\n                    \"label\": \"VMs # (Avg.)\"\n                  },\n                  {\n                    \"columnId\": \"Fragmentation\",\n                    \"label\": \"Fragmentation\"\n                  },\n                  {\n                    \"columnId\": \"AvgSizeUsageHours\",\n                    \"label\": \"Usage (Avg. Hrs.)\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"45\",\n            \"name\": \"riPotential\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend VMSize=tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| where ISFGroup == '{ISFGroup}' and ResourceLocation_s =~ '{Location}'\\r\\n| summarize UsedQuantity = round(sum(todouble(Quantity_s)/24*Ratio)) by todatetime(Date_s)\\r\\n| extend RIPotential = {RIPotential}\\r\\n| render timechart\",\n              \"size\": 0,\n              \"aggregation\": 3,\n              \"title\": \"Instance count for selected ISF Group/location (click on a line in the table at the left)\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"chartSettings\": {\n                \"seriesLabelSettings\": [\n                  {\n                    \"seriesName\": \"UsedQuantity\",\n                    \"label\": \"Instance #\"\n                  },\n                  {\n                    \"seriesName\": \"RIPotential\",\n                    \"label\": \"RI Potential\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"55\",\n            \"name\": \"riPotentialOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize=tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{ISFGroup}' and ResourceLocation_s =~ '{Location}'\\r\\n| summarize UsedQuantity = sum(todouble(Quantity_s)) by Date_s, ResourceId, SubscriptionId, VMSize, Ratio\\r\\n| summarize AvgUsedQuantity = round(avg(UsedQuantity),1) by ResourceId, SubscriptionId, VMSize, Ratio\\r\\n| join kind=leftouter ( AzureOptimizationResourceContainersV1_CL | where TimeGenerated > ago(1d) and ContainerType_s =~ 'microsoft.resources/subscriptions' | project SubscriptionName=ContainerName_s, SubscriptionId=SubscriptionGuid_g) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder ResourceId, SubscriptionName\\r\\n| extend AvgUsedVMs = AvgUsedQuantity / 24\\r\\n| order by ResourceId asc\",\n              \"size\": 1,\n              \"title\": \"Daily on-demand usage for selected ISF group/location by resource (click on a line in the table above)\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ResourceId\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionName\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"VMSize\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Ratio\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"10ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedQuantity\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"12ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedVMs\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"VMSize\",\n                    \"label\": \"Size\"\n                  },\n                  {\n                    \"columnId\": \"Ratio\",\n                    \"label\": \"Ratio\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedQuantity\",\n                    \"label\": \"Avg. hrs\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedVMs\",\n                    \"label\": \"Avg. VMs\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riPotentialInstances\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and ChargeType_s == 'Usage'\\r\\n| where ConsumedService_s in ('Microsoft.Compute','Microsoft.ClassicCompute','Microsoft.Batch','Microsoft.MachineLearningServices','Microsoft.Kusto')\\r\\n| where MeterCategory_s != 'Virtual Machines Licenses'\\r\\n| extend PricingModel = iif(isnotempty(PricingModel_s), PricingModel_s, iif(isnotempty(ReservationName_s), 'Reservation', iif(MeterName_s endswith 'Spot', 'Spot', iif(isnotempty(benefitName_s), 'SavingsPlan', 'OnDemand'))))\\r\\n| where PricingModel == 'OnDemand'\\r\\n| extend VMSize=tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ResourceLocation_s = tolower(ResourceLocation_s)\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{ISFGroup}' and ResourceLocation_s =~ '{Location}'\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{AggregatorTag}'])\\r\\n| summarize UsedQuantity = sum(todouble(Quantity_s)) by Date_s, AggregatorTag, SubscriptionId, VMSize, Ratio\\r\\n| summarize AvgUsedQuantity = round(avg(UsedQuantity),1) by AggregatorTag, SubscriptionId, VMSize, Ratio\\r\\n| join kind=leftouter ( AzureOptimizationResourceContainersV1_CL | where TimeGenerated > ago(1d) and ContainerType_s =~ 'microsoft.resources/subscriptions' | project SubscriptionName=ContainerName_s, SubscriptionId=SubscriptionGuid_g) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder AggregatorTag, SubscriptionName\\r\\n| extend AvgUsedVMs = AvgUsedQuantity / 24\\r\\n| order by AggregatorTag asc\",\n              \"size\": 1,\n              \"title\": \"Daily on-demand usage for selected ISF group/location by tag (click on a line in the table above)\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"AggregatorTag\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"19ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SubscriptionName\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"VMSize\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Ratio\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"10ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedQuantity\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"12ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgUsedVMs\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ResourceId\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"22ch\"\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"SubscriptionName\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"VMSize\",\n                    \"label\": \"Size\"\n                  },\n                  {\n                    \"columnId\": \"Ratio\",\n                    \"label\": \"Ratio\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedQuantity\",\n                    \"label\": \"Avg. hrs\"\n                  },\n                  {\n                    \"columnId\": \"AvgUsedVMs\",\n                    \"label\": \"Avg. VMs\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riPotentialTags\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"UseISF\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Yes\"\n      },\n      \"name\": \"yesISFGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/reservations-usage.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Reservations Usage'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '5fc75aa1-db43-4938-bbea-90dcb71ef5a2'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('reservations-usage.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/reservations-usage.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"b58b4eb8-5821-44d2-bc7e-54054df27320\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LookbackPeriod\",\n            \"label\": \"Lookback Period\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          },\n          {\n            \"id\": \"08c51931-8b6c-419d-8de2-f9d24e8e6dd7\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"ResourceType\",\n            \"label\": \"Resource Type\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationReservationsUsageV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ProvisioningState_s == 'Succeeded'\\r\\n| distinct ResourceType\\r\\n| order by ResourceType asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            },\n            \"timeContextFromParameter\": \"LookbackPeriod\",\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"id\": \"5b2d78e9-7177-4d9b-86fa-2a9b12dd470a\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Reservation\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationReservationsUsageV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ProvisioningState_s == 'Succeeded' and ResourceType in ({ResourceType:value})\\r\\n| distinct ReservationId_g, DisplayName_s\\r\\n| order by DisplayName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            },\n            \"timeContextFromParameter\": \"LookbackPeriod\",\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Aggregator\",\n            \"label\": \"Aggregator Tag\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"timeContext\": {\n              \"durationMs\": 2592000000\n            },\n            \"id\": \"a3fb4877-28ef-43fc-8821-376df486fa2a\"\n          },\n          {\n            \"id\": \"3e36a073-14b2-4406-a84a-1b6d0a15f363\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"UseISF\",\n            \"label\": \"Instance Size Flexibility?\",\n            \"type\": 10,\n            \"description\": \"Groups reservation by ISF (excludes non-VM reservations)\",\n            \"isRequired\": true,\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [],\n              \"showDefault\": false\n            },\n            \"jsonData\": \"[\\\"Yes\\\", \\\"No\\\"]\",\n            \"value\": \"No\"\n          }\n        ],\n        \"style\": \"above\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Consumption data is updated once every 24 hours and is presented in the currency of your Azure consumption agreement.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"text - 7\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).\",\n        \"style\": \"warning\"\n      },\n      \"name\": \"text - 10\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"93e7a6c7-cb1f-49ee-b135-468b9f528b04\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Reservation Usage Analysis\",\n            \"subTarget\": \"reservationAnalysis\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"abd2af9f-2f88-45a2-9d09-4b439e736a71\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Usage by Tag\",\n            \"subTarget\": \"usageByTag\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"96332944-d3f7-4b0b-ad56-ab25a9e91049\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Full Usage Report\",\n            \"subTarget\": \"fullReport\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"1f438af9-e6ff-470e-9b11-b1b5a701e51b\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Unused Reservations Analysis\",\n            \"subTarget\": \"unusedReservations\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"tabs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandUnitPrice;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\\r\\n| distinct MeterID_g, OnDemandUnitPrice\\r\\n| union (VMOnDemandPriceSheet)\\r\\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\\r\\nlet ReservationPricesheet = AzureOptimizationReservationsPriceV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| extend Term_s = iif(reservationTerm_s == '3 Years', 'P3Y', 'P1Y')\\r\\n| extend TermDivider = iif(reservationTerm_s == '3 Years', 3, 1)\\r\\n| extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\\r\\n| summarize arg_max(TimeGenerated, ReservationPrice) by Location_s=tolower(armRegionName_s), SkuName=tolower(armSkuName_s), Term_s;\\r\\nlet ReservationOnDemandMeters = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| extend SkuName = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| distinct ReservationId_g, MeterId_g, SkuName;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize UsedRIsDaily=round(sum(UsedRIs),2) by Date_s, ReservationId_g\\r\\n| summarize AvgRIsUsedDaily=round(avg(UsedRIsDaily),2) by ReservationId_g\\r\\n| join kind=rightouter (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ReservationId_g in ({Reservation:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by ReservationId_g\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s, Location_s, Util7Days_s, Util30Days_s, TotalReservedQuantity_s, Term_s\\r\\n) on ReservationId_g\\r\\n| project ReservationId_g=ReservationId_g1, ReservationName_s, TotalReservedQuantity_s, SKUName_s=tolower(SKUName_s), Location_s, AvgRIsUsedDaily=iif(isempty(AvgRIsUsedDaily), 0.0, AvgRIsUsedDaily), Util7Days_s, Util30Days_s, Term_s\\r\\n| join kind=leftouter (ReservationOnDemandMeters) on ReservationId_g\\r\\n| summarize arg_max(MeterId_g, *) by ReservationId_g\\r\\n| join kind=leftouter (ReservationPricesheet) on SkuName and Location_s and Term_s\\r\\n| join kind=leftouter (OnDemandPriceSheet) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend DiscountPercent = (1 - ReservationPrice/OnDemandUnitPrice) * 100\\r\\n| project ReservationId_g, ReservationName_s, TotalReservedQuantity_s=round(todouble(TotalReservedQuantity_s)), SKUName_s, Location_s, AvgRIsUsedDaily, Util7Days_s=round(todouble(Util7Days_s)), Util30Days_s=round(todouble(Util30Days_s)), DiscountPercent, SavingsMargin=round(todouble(Util7Days_s))-100.0+DiscountPercent \\r\\n| order by Util7Days_s asc\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Reservation Usage Details (click on a line for more details)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"ReservationId_g\",\n                  \"parameterName\": \"selectedReservation\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"TotalReservedQuantity_s\",\n                  \"parameterName\": \"selectedQuantity\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ReservationName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"17ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SKUName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Location_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"DiscountPercent\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SavingsMargin\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"colors\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"0\",\n                          \"representation\": \"redBright\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"5\",\n                          \"representation\": \"yellow\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"green\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ],\n                      \"customColumnWidthSetting\": \"12ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"sortBy\": [\n                  {\n                    \"itemKey\": \"$gen_number_Util7Days_s_6\",\n                    \"sortOrder\": 1\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ReservationName_s\",\n                    \"label\": \"Reservation\"\n                  },\n                  {\n                    \"columnId\": \"TotalReservedQuantity_s\",\n                    \"label\": \"Qty.\"\n                  },\n                  {\n                    \"columnId\": \"SKUName_s\",\n                    \"label\": \"Size\"\n                  },\n                  {\n                    \"columnId\": \"Location_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"AvgRIsUsedDaily\",\n                    \"label\": \"Qty. Used (Avg)\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"DiscountPercent\",\n                    \"label\": \"Discount\"\n                  },\n                  {\n                    \"columnId\": \"SavingsMargin\",\n                    \"label\": \"Savings\"\n                  }\n                ]\n              },\n              \"sortBy\": [\n                {\n                  \"itemKey\": \"$gen_number_Util7Days_s_6\",\n                  \"sortOrder\": 1\n                }\n              ]\n            },\n            \"customWidth\": \"55\",\n            \"name\": \"riUsageDetailsV2\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g == '{selectedReservation:value}'\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend VMSize = tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ConsumedSize = iif(isnotempty(VMSize), VMSize, strcat(MeterSubCategory_s, ' ', MeterName_s))\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize round(sum(UsedRIs),1) by todatetime(Date_s), ConsumedSize\",\n              \"size\": 0,\n              \"aggregation\": 3,\n              \"showAnalytics\": true,\n              \"title\": \"Average RI Usage Count by Size (click on a line in the table at the left)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 1000\n              },\n              \"chartSettings\": {\n                \"group\": \"ConsumedSize\",\n                \"createOtherGroup\": null,\n                \"customThresholdLine\": \"{selectedQuantity}\",\n                \"customThresholdLineStyle\": 1\n              }\n            },\n            \"customWidth\": \"45\",\n            \"name\": \"riUsageDailyAverageBySize\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\\r\\n    and ChargeType_s == 'Usage'\\r\\n    and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g == '{selectedReservation:value}'\\r\\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g == '{selectedReservation:value}'\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by ResourceId, SubscriptionId\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder ResourceId, Subscription, UsedRIPercentage\\r\\n| order by ResourceId asc, UsedRIPercentage\",\n              \"size\": 1,\n              \"showAnalytics\": true,\n              \"title\": \"Average Daily Reservation Usage by Resource (click on a line in the table above)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource\"\n                  },\n                  {\n                    \"columnId\": \"Subscription\",\n                    \"label\": \"Used RI (%)\"\n                  },\n                  {\n                    \"columnId\": \"UsedRIPercentage\",\n                    \"label\": \"Used RI (%)\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riUsageDailyAverageByResource\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\\r\\n    and ChargeType_s == 'Usage'\\r\\n    and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g == '{selectedReservation:value}'\\r\\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g == '{selectedReservation:value}'\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by AggregatorTag, SubscriptionId\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder AggregatorTag, Subscription, UsedRIPercentage\\r\\n| order by AggregatorTag asc, UsedRIPercentage\",\n              \"size\": 1,\n              \"showAnalytics\": true,\n              \"title\": \"Average Daily Reservation Usage by Tag (click on a line in the table above)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"UsedRIPercentage\",\n                    \"label\": \"Used RI (%)\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riUsageDailyAverageByInstance\"\n          }\n        ]\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"reservationAnalysis\"\n        },\n        {\n          \"parameterName\": \"UseISF\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"No\"\n        }\n      ],\n      \"name\": \"reservationAnalysisGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"VM count for Instance Size Flexibility Groups is proportional to the VM size with the lowest ratio (1).\",\n              \"style\": \"warning\"\n            },\n            \"name\": \"text - 3\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nlet LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandUnitPrice;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\\r\\n| distinct MeterID_g, OnDemandUnitPrice\\r\\n| union (VMOnDemandPriceSheet)\\r\\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\\r\\nlet ReservationPricesheet = AzureOptimizationReservationsPriceV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| extend Term_s = iif(reservationTerm_s == '3 Years', 'P3Y', 'P1Y')\\r\\n| extend TermDivider = iif(reservationTerm_s == '3 Years', 3, 1)\\r\\n| extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\\r\\n| summarize arg_max(TimeGenerated, ReservationPrice) by Location_s=tolower(armRegionName_s), SkuName=tolower(armSkuName_s), Term_s;\\r\\nlet ReservationOnDemandMeters = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| extend SkuName = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| distinct ReservationId_g, MeterId_g, SkuName;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize UsedRIsDaily=round(sum(UsedRIs),2) by Date_s, ReservationId_g\\r\\n| summarize AvgRIsUsedDaily=round(avg(UsedRIsDaily),2) by ReservationId_g\\r\\n| join kind=rightouter (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ReservationId_g in ({Reservation:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by ReservationId_g\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | extend UsedQuantity = todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100\\r\\n    | extend UsedQuantity30d = todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100\\r\\n    | extend SKUName_s=tolower(SKUName_s)\\r\\n    | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s, Location_s, UsedQuantity, UsedQuantity30d, TotalReservedQuantity_s, Term_s, AppliedScopeType_s\\r\\n) on ReservationId_g\\r\\n| project ReservationId_g=ReservationId_g1, ReservationName_s, TotalReservedQuantity_s, SKUName_s, Location_s, AvgRIsUsedDaily=iif(isempty(AvgRIsUsedDaily), 0.0, AvgRIsUsedDaily), UsedQuantity, UsedQuantity30d, Term_s, AppliedScopeType_s\\r\\n| join kind=inner ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\\r\\n| join kind=leftouter ( ReservationOnDemandMeters ) on ReservationId_g\\r\\n| summarize arg_max(MeterId_g, *) by ReservationId_g\\r\\n| join kind=leftouter ( ReservationPricesheet ) on SkuName and Location_s and Term_s\\r\\n| join kind=leftouter ( OnDemandPriceSheet ) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend DiscountPercent = (1 - ReservationPrice/OnDemandUnitPrice) * 100\\r\\n| extend AvgRIsUsedInSmallestRatio = Ratio * AvgRIsUsedDaily\\r\\n| summarize TotalReservedQuantity_s=sum(todouble(TotalReservedQuantity_s)*Ratio), AvgRIsUsedDaily=sum(AvgRIsUsedInSmallestRatio), UsedQuantity=sum(UsedQuantity*Ratio), UsedQuantity30d=sum(UsedQuantity30d*Ratio), AvgDiscountPercent=avg(DiscountPercent) by ISFGroup, Location_s, Term_s, AppliedScopeType_s\\r\\n| extend Util7Days_s = UsedQuantity/TotalReservedQuantity_s*100, Util30Days_s = UsedQuantity30d/TotalReservedQuantity_s*100\\r\\n| extend AvgRIUsagePercentInSmallestRatio = round(AvgRIsUsedDaily / TotalReservedQuantity_s * 100, 1)\\r\\n| extend AvgDiscountPercent=iif(AvgDiscountPercent > 0.0, AvgDiscountPercent, 0.0)\\r\\n| extend SavingsMargin=round(todouble(Util7Days_s))-100.0+AvgDiscountPercent \\r\\n| project-away AvgRIUsagePercentInSmallestRatio, AvgRIsUsedDaily\\r\\n| project-reorder ISFGroup, Location_s, Term_s, AppliedScopeType_s, TotalReservedQuantity_s, Util7Days_s, UsedQuantity, Util30Days_s, UsedQuantity30d\\r\\n| order by Util7Days_s asc\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Reservation Usage Details grouped by ISF group (click on a line for more details)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"ISFGroup\",\n                  \"parameterName\": \"selectedISFGroup\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"TotalReservedQuantity_s\",\n                  \"parameterName\": \"selectedQuantity\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"Location_s\",\n                  \"parameterName\": \"selectedRegion\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ISFGroup\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Location_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AppliedScopeType_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"11ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"10ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UsedQuantity\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UsedQuantity30d\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgDiscountPercent\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SavingsMargin\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"colors\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"0\",\n                          \"representation\": \"redBright\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"5\",\n                          \"representation\": \"yellow\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"green\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ],\n                      \"customColumnWidthSetting\": \"12ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgRIsUsedDaily\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AvgRIUsagePercentInSmallestRatio\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 1,\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ReservationName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"30ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SKUName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"sortBy\": [\n                  {\n                    \"itemKey\": \"$gen_number_Util7Days_s_5\",\n                    \"sortOrder\": 1\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ISFGroup\",\n                    \"label\": \"ISF Group\"\n                  },\n                  {\n                    \"columnId\": \"Location_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"Term_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"AppliedScopeType_s\",\n                    \"label\": \"Scope\"\n                  },\n                  {\n                    \"columnId\": \"TotalReservedQuantity_s\",\n                    \"label\": \"Qty.\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"UsedQuantity\",\n                    \"label\": \"Used Qty. (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"UsedQuantity30d\",\n                    \"label\": \"Used Qty. (30d)\"\n                  },\n                  {\n                    \"columnId\": \"AvgDiscountPercent\",\n                    \"label\": \"Avg. Discount\"\n                  },\n                  {\n                    \"columnId\": \"SavingsMargin\",\n                    \"label\": \"Savings\"\n                  }\n                ]\n              },\n              \"sortBy\": [\n                {\n                  \"itemKey\": \"$gen_number_Util7Days_s_5\",\n                  \"sortOrder\": 1\n                }\n              ]\n            },\n            \"customWidth\": \"65\",\n            \"name\": \"riUsageDetailsV2ISF\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ResourceLocation_s =~ '{selectedRegion}'\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{selectedISFGroup:value}'\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend ConsumedSize = iif(isnotempty(VMSize), VMSize, strcat(MeterSubCategory_s, ' ', MeterName_s))\\r\\n| extend UsedRIs = todouble(Quantity_s) / 24 * Ratio\\r\\n| summarize round(sum(UsedRIs),1) by todatetime(Date_s), ConsumedSize\",\n              \"size\": 0,\n              \"aggregation\": 3,\n              \"showAnalytics\": true,\n              \"title\": \"Average RI Usage Count by Size (click on a line in the table at the left)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 1000\n              },\n              \"chartSettings\": {\n                \"group\": \"ConsumedSize\",\n                \"createOtherGroup\": null,\n                \"customThresholdLine\": \"{selectedQuantity}\",\n                \"customThresholdLineStyle\": 1\n              }\n            },\n            \"customWidth\": \"35\",\n            \"name\": \"riUsageDailyAverageBySizeISF\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nlet absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\\r\\n    and ChargeType_s == 'Usage'\\r\\n    and isnotempty(ReservationName_s)\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| where ResourceLocation_s =~ '{selectedRegion}'\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{selectedISFGroup:value}'\\r\\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| where ResourceLocation_s =~ '{selectedRegion}'\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{selectedISFGroup:value}'\\r\\n| extend UsedRIs = todouble(Quantity_s) * Ratio / 24\\r\\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by ResourceId, SubscriptionId\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder ResourceId, Subscription, UsedRIPercentage\\r\\n| order by ResourceId asc, UsedRIPercentage\",\n              \"size\": 1,\n              \"showAnalytics\": true,\n              \"title\": \"Average Daily Reservation Usage by Resource (click on a line in the table above)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource\"\n                  },\n                  {\n                    \"columnId\": \"Subscription\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"UsedRIPercentage\",\n                    \"label\": \"Used RI (%)\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riUsageDailyAverageByResourceISF\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nlet absoluteRIBought = toscalar(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\\r\\n    and ChargeType_s == 'Usage'\\r\\n    and isnotempty(ReservationName_s)\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| where ResourceLocation_s =~ '{selectedRegion}'\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{selectedISFGroup:value}'\\r\\n| summarize (datetime_diff('Day',max(todatetime(Date_s)),min(todatetime(Date_s)))+1)*{selectedQuantity});\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| extend VMSize = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| where ResourceLocation_s =~ '{selectedRegion}'\\r\\n| join kind=inner ( ISFGroups ) on $left.VMSize == $right.ArmSKUName\\r\\n| where ISFGroup == '{selectedISFGroup:value}'\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\\r\\n| extend UsedRIs = todouble(Quantity_s) * Ratio / 24\\r\\n| summarize UsedRIPercentage=round(sum(UsedRIs) / absoluteRIBought * 100, 2) by AggregatorTag, SubscriptionId\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder AggregatorTag, Subscription, UsedRIPercentage\\r\\n| order by AggregatorTag asc, UsedRIPercentage\",\n              \"size\": 1,\n              \"showAnalytics\": true,\n              \"title\": \"Average Daily Reservation Usage by Tag (click on a line in the table above)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"Subscription\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"UsedRIPercentage\",\n                    \"label\": \"Used RI (%)\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"riUsageDailyAverageByInstanceISF\"\n          }\n        ]\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"reservationAnalysis\"\n        },\n        {\n          \"parameterName\": \"UseISF\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"Yes\"\n        }\n      ],\n      \"name\": \"reservationAnalysisGroupISF\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let DaysSeenByReservation = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| summarize DaysSeen=datetime_diff('Day', max(todatetime(Date_s)), min(todatetime(Date_s)))+1 by ReservationId_g;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize TotalUsedRIs=round(sum(UsedRIs),2) by ReservationId_g, AggregatorTag, SubscriptionId\\r\\n| join kind=inner (DaysSeenByReservation) on ReservationId_g\\r\\n| project-away ReservationId_g1\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| join kind=inner (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    | where ReservationId_g in ({Reservation:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by ReservationId_g\\r\\n    | project ReservationId_g, TotalReservedQuantity_s, ReservationName_s=DisplayName_s\\r\\n) on ReservationId_g\\r\\n| project-away ReservationId_g1\\r\\n| extend UsedRIPercentage = TotalUsedRIs / (todouble(TotalReservedQuantity_s) * DaysSeen) * 100\\r\\n| project-reorder ReservationName_s, AggregatorTag\\r\\n| order by ReservationId_g\",\n              \"size\": 2,\n              \"showAnalytics\": true,\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"ReservationId_g\",\n                  \"parameterName\": \"selectedReservation\"\n                },\n                {\n                  \"fieldName\": \"ReservationName_s\",\n                  \"parameterName\": \"selectedReservationName\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalUsedRIs\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"DaysSeen\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g1\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"UsedRIPercentage\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ReservationName_s\",\n                    \"label\": \"Reservation\"\n                  },\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"UsedRIPercentage\",\n                    \"label\": \"Used RI\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"riUsageByTag\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"usageByTag\"\n      },\n      \"name\": \"usageByTagGroup\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend UnusedCost = todouble(CostInBillingCurrency_s)\\r\\n| project todatetime(Date_s), ReservationName_s, UnusedCost\",\n        \"size\": 0,\n        \"title\": \"Cost of Unused Reservations over time\",\n        \"timeContextFromParameter\": \"LookbackPeriod\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"barchart\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"unusedReservations\"\n      },\n      \"name\": \"unusedReservationsOverTime\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend UnusedCost = todouble(CostInBillingCurrency_s)\\r\\n| join kind=leftouter (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | extend UnusedQuantity = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100)\\r\\n    | extend UnusedQuantity30d = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100)\\r\\n    | project ReservationId_g, SKUName_s=tolower(SKUName_s), Location_s\\r\\n) on ReservationId_g\\r\\n| join kind=leftouter ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\\r\\n| extend SKUName = iif('{UseISF}' == 'Yes', strcat(ISFGroup, \\\" \\\", Location_s), strcat(SKUName_s, \\\" \\\", Location_s))\\r\\n| extend SKUName = iif(strlen(SKUName) < 2, 'Canceled RIs', SKUName)\\r\\n| summarize sum(UnusedCost) by todatetime(Date_s), SKUName\",\n        \"size\": 0,\n        \"showAnalytics\": true,\n        \"title\": \"Cost of Unused Reservations over time (by SKU)\",\n        \"timeContextFromParameter\": \"LookbackPeriod\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"barchart\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"unusedReservations\"\n      },\n      \"name\": \"unusedReservationsOverTimebySKU\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Canceled RIs are not considered in the historical view. VM count for Instance Size Flexibility Groups is proportional to the VM size with the lowest ratio (1).\",\n        \"style\": \"warning\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"unusedReservations\"\n      },\n      \"name\": \"warningISFCanceled\"\n    },\n    {\n      \"type\": 3,\n      \"content\": {\n        \"version\": \"KqlItem/1.0\",\n        \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend UnusedHours = todouble(Quantity_s)\\r\\n| join kind=inner (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | extend UnusedQuantity = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100)\\r\\n    | extend UnusedQuantity30d = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100)\\r\\n    | project ReservationId_g, SKUName_s=tolower(SKUName_s), Location_s\\r\\n) on ReservationId_g\\r\\n| join kind=leftouter ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\\r\\n| extend SKUName = iif('{UseISF}' == 'Yes', strcat(ISFGroup, \\\" \\\", Location_s), strcat(SKUName_s, \\\" \\\", Location_s))\\r\\n| extend UnusedVMs = iif('{UseISF}' == 'Yes', UnusedHours * Ratio / 24, UnusedHours / 24)\\r\\n| summarize sum(UnusedVMs) by todatetime(Date_s), SKUName\",\n        \"size\": 0,\n        \"showAnalytics\": true,\n        \"title\": \"Unused Reservations over time (by VM count)\",\n        \"timeContextFromParameter\": \"LookbackPeriod\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n        \"visualization\": \"barchart\"\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"unusedReservations\"\n      },\n      \"name\": \"unusedReservationsOverTimebyISFGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| summarize TotalUnusedCost = sum(todouble(CostInBillingCurrency_s)) by ReservationId_g\\r\\n| where round(TotalUnusedCost) > 0\\r\\n| join kind=inner (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | extend UnusedQuantity = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100)\\r\\n    | extend UnusedQuantity30d = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100)\\r\\n    | project ReservationId_g, ReservationName_s=DisplayName_s, ExpiryDate_s, SKUName_s, Location_s, TotalReservedQuantity_s, Term_s, AppliedScopeType_s, Util7Days_s, UnusedQuantity, Util30Days_s, UnusedQuantity30d\\r\\n) on ReservationId_g\\r\\n| project-away ReservationId_g1\\r\\n| project-reorder ReservationName_s, TotalUnusedCost\\r\\n| order by TotalUnusedCost\\r\\n\",\n              \"size\": 2,\n              \"showAnalytics\": true,\n              \"title\": \"Unused Reservations Details\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"TotalUnusedCost\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"22ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 1,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 1,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UnusedQuantity\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 17,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ProvisioningState_s\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Cancelled\",\n                          \"representation\": \"4\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ReservationName_s\",\n                    \"label\": \"Reservation\"\n                  },\n                  {\n                    \"columnId\": \"TotalUnusedCost\",\n                    \"label\": \"Total Unused Cost\"\n                  },\n                  {\n                    \"columnId\": \"ExpiryDate_s\",\n                    \"label\": \"Expires On\"\n                  },\n                  {\n                    \"columnId\": \"SKUName_s\",\n                    \"label\": \"Size\"\n                  },\n                  {\n                    \"columnId\": \"Location_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"TotalReservedQuantity_s\",\n                    \"label\": \"Qty.\"\n                  },\n                  {\n                    \"columnId\": \"Term_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"AppliedScopeType_s\",\n                    \"label\": \"Scope\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"UnusedQuantity\",\n                    \"label\": \"Unused Qty. (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"UnusedQuantity30d\",\n                    \"label\": \"Unused Qty. (30d)\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"unusedReservationsDetails\"\n          }\n        ]\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"unusedReservations\"\n        },\n        {\n          \"parameterName\": \"UseISF\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"No\"\n        }\n      ],\n      \"name\": \"unusedReservationsGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 1,\n            \"content\": {\n              \"json\": \"VM count for Instance Size Flexibility Groups is proportional to the VM size with the lowest ratio (1).\",\n              \"style\": \"warning\"\n            },\n            \"name\": \"text - 1\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend ArmSKUName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedReservation'\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| summarize TotalUnusedCost = sum(todouble(CostInBillingCurrency_s)) by ReservationId_g\\r\\n| where round(TotalUnusedCost) > 0\\r\\n| join kind=inner (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | extend UnusedQuantity = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util7Days_s) / 100)\\r\\n    | extend UnusedQuantity30d = todouble(TotalReservedQuantity_s) - (todouble(TotalReservedQuantity_s) * todouble(Util30Days_s) / 100)\\r\\n    | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s=tolower(SKUName_s), Location_s, TotalReservedQuantity_s, Term_s, AppliedScopeType_s, UnusedQuantity, UnusedQuantity30d\\r\\n) on ReservationId_g\\r\\n| project-away ReservationId_g1\\r\\n| join kind=inner ( ISFGroups ) on $left.SKUName_s == $right.ArmSKUName\\r\\n| summarize TotalUnusedCost=sum(TotalUnusedCost), TotalReservedQuantity_s=sum(todouble(TotalReservedQuantity_s)*Ratio), UnusedQuantity=sum(UnusedQuantity*Ratio), UnusedQuantity30d=sum(UnusedQuantity30d*Ratio) by ISFGroup, Location_s, Term_s, AppliedScopeType_s\\r\\n| extend Util7Days_s = (1-UnusedQuantity/TotalReservedQuantity_s)*100, Util30Days_s = (1-UnusedQuantity30d/TotalReservedQuantity_s)*100\\r\\n| project-reorder ISFGroup, TotalUnusedCost, Location_s, Term_s, AppliedScopeType_s, TotalReservedQuantity_s, Util7Days_s, UnusedQuantity, Util30Days_s\\r\\n| order by TotalUnusedCost\",\n              \"size\": 2,\n              \"showAnalytics\": true,\n              \"title\": \"Unused Reservations Details (grouped by Instance Size Flexibility group)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"TotalUnusedCost\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"22ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 1,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 1,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UnusedQuantity\",\n                    \"formatter\": 1,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ReservationName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ProvisioningState_s\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"icons\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"==\",\n                          \"thresholdValue\": \"Cancelled\",\n                          \"representation\": \"4\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"success\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    }\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ISFGroup\",\n                    \"label\": \"ISF Group\"\n                  },\n                  {\n                    \"columnId\": \"TotalUnusedCost\",\n                    \"label\": \"Total Unused Cost\"\n                  },\n                  {\n                    \"columnId\": \"Location_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"Term_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"AppliedScopeType_s\",\n                    \"label\": \"Scope\"\n                  },\n                  {\n                    \"columnId\": \"TotalReservedQuantity_s\",\n                    \"label\": \"Qty.\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"UnusedQuantity\",\n                    \"label\": \"Unused Qty. (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"UnusedQuantity30d\",\n                    \"label\": \"Unused Qty. (30d)\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"unusedReservationsDetailsISF\"\n          }\n        ]\n      },\n      \"conditionalVisibilities\": [\n        {\n          \"parameterName\": \"selectedTab\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"unusedReservations\"\n        },\n        {\n          \"parameterName\": \"UseISF\",\n          \"comparison\": \"isEqualTo\",\n          \"value\": \"Yes\"\n        }\n      ],\n      \"name\": \"unusedReservationsGroupISF\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandUnitPrice;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\\r\\n| union (VMOnDemandPriceSheet)\\r\\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\\r\\nlet ReservationPricesheet = materialize(AzureOptimizationReservationsPriceV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| extend Term_s = iif(reservationTerm_s == '3 Years', 'P3Y', 'P1Y')\\r\\n| extend TermDivider = iif(reservationTerm_s == '3 Years', 3, 1)\\r\\n| extend ReservationPrice = todouble(replace_string(unitPrice_s,',','.'))/TermDivider/12/730\\r\\n| summarize arg_max(TimeGenerated, ReservationPrice) by Location_s=tolower(armRegionName_s), SkuName=tolower(armSkuName_s), Term_s);\\r\\nlet ReservationOnDemandMeters = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| extend SkuName = tolower(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| distinct ReservationId_g, MeterId_g, SkuName;\\r\\nlet ISFGroups = externaldata(ISFGroup:string, ArmSKUName:string, Ratio:double) [@\\\"https://aka.ms/isf\\\"] with(ignoreFirstRecord=true) | extend skuName=tolower(ArmSKUName);\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and isnotempty(ReservationName_s)\\r\\n| where ReservationId_g in ({Reservation:value})\\r\\n| extend RINormalizationRatio = tostring(parse_json(AdditionalInfo_s).RINormalizationRatio)\\r\\n| extend UsedRIs = todouble(Quantity_s) * todouble(RINormalizationRatio) / 24\\r\\n| summarize UsedRIsDaily=round(sum(UsedRIs),2) by Date_s, ReservationId_g\\r\\n| summarize AvgRIsUsedDaily=round(avg(UsedRIsDaily),2) by ReservationId_g\\r\\n| join kind=rightouter (\\r\\n    AzureOptimizationReservationsUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where ReservationId_g in ({Reservation:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by ReservationId_g\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | project ReservationId_g, ReservationName_s=DisplayName_s, SKUName_s, Location_s, Util7Days_s, Util30Days_s, TotalReservedQuantity_s, Term_s, ExpiryDate_s\\r\\n) on ReservationId_g\\r\\n| project ReservationId_g=ReservationId_g1, ReservationName_s, TotalReservedQuantity_s, SKUName_s=tolower(SKUName_s), Location_s, AvgRIsUsedDaily=iif(isempty(AvgRIsUsedDaily), 0.0, AvgRIsUsedDaily), Util7Days_s, Util30Days_s, Term_s, ExpiryDate_s\\r\\n| join kind=leftouter ( ISFGroups ) on $left.SKUName_s==$right.skuName\\r\\n| join kind=leftouter (ReservationOnDemandMeters) on ReservationId_g\\r\\n| summarize arg_max(MeterId_g, *) by ReservationId_g\\r\\n| join kind=leftouter (ReservationPricesheet) on SkuName and Location_s and Term_s\\r\\n| join kind=leftouter (OnDemandPriceSheet) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend DiscountPercent = (1 - ReservationPrice/OnDemandUnitPrice) * 100\\r\\n| project-away ReservationPrice\\r\\n| join kind=leftouter (ReservationPricesheet) on $left.SKUName_s==$right.SkuName and Location_s and Term_s\\r\\n| extend HoursUntilExpiry=(todatetime(ExpiryDate_s)-now())/1h\\r\\n| extend TotalReservedHoursToConsume=todouble(TotalReservedQuantity_s)*HoursUntilExpiry\\r\\n| extend AmountRemainingToConsume = round(TotalReservedHoursToConsume * ReservationPrice, 2)\\r\\n| project ReservationId_g, ReservationName_s, TotalReservedQuantity_s=round(todouble(TotalReservedQuantity_s)), SKUName_s, ISFGroup, Location_s, Term_s, ExpiryDate_s, AmountRemainingToConsume, AvgRIsUsedDaily, Util7Days_s=round(todouble(Util7Days_s)), Util30Days_s=round(todouble(Util30Days_s)), DiscountPercent, SavingsMargin=round(todouble(Util7Days_s))-100.0+DiscountPercent \\r\\n| order by Util7Days_s asc\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"ReservationId_g\",\n                  \"parameterName\": \"selectedReservation\",\n                  \"parameterType\": 1\n                },\n                {\n                  \"fieldName\": \"TotalReservedQuantity_s\",\n                  \"parameterName\": \"selectedQuantity\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ReservationName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"24ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"9ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SKUName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"18ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ISFGroup\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Location_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AmountRemainingToConsume\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    },\n                    \"tooltipFormat\": {\n                      \"tooltip\": \"Remaining commitment to be consumed until the reservation expiry date (value in the currency of your consumption agreement)\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"DiscountPercent\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SavingsMargin\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"colors\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"0\",\n                          \"representation\": \"redBright\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"5\",\n                          \"representation\": \"yellow\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"green\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ],\n                      \"customColumnWidthSetting\": \"12ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ReservationId_g\",\n                    \"label\": \"ID\"\n                  },\n                  {\n                    \"columnId\": \"ReservationName_s\",\n                    \"label\": \"Reservation\"\n                  },\n                  {\n                    \"columnId\": \"TotalReservedQuantity_s\",\n                    \"label\": \"Qty.\"\n                  },\n                  {\n                    \"columnId\": \"SKUName_s\",\n                    \"label\": \"Size\"\n                  },\n                  {\n                    \"columnId\": \"ISFGroup\",\n                    \"label\": \"ISF Group\"\n                  },\n                  {\n                    \"columnId\": \"Location_s\",\n                    \"label\": \"Region\"\n                  },\n                  {\n                    \"columnId\": \"Term_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"ExpiryDate_s\",\n                    \"label\": \"Expires On\"\n                  },\n                  {\n                    \"columnId\": \"AmountRemainingToConsume\",\n                    \"label\": \"Remain. Commit.\",\n                    \"comment\": \"\"\n                  },\n                  {\n                    \"columnId\": \"AvgRIsUsedDaily\",\n                    \"label\": \"Qty. Used (Avg)\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"DiscountPercent\",\n                    \"label\": \"Discount\"\n                  },\n                  {\n                    \"columnId\": \"SavingsMargin\",\n                    \"label\": \"Savings\"\n                  }\n                ]\n              },\n              \"sortBy\": []\n            },\n            \"name\": \"riUsageDetailsFull\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"fullReport\"\n      },\n      \"name\": \"fullReport\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/resources-inventory.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Resources Inventory'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = '065fc198-6435-4724-99b2-60cea8a2d7d2'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('resources-inventory.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/resources-inventory.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"d2503809-fae8-47d2-953c-3a2255d0d9fc\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"ResourcesTimeRange\",\n            \"label\": \"Time Range\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 86400000\n                },\n                {\n                  \"durationMs\": 172800000\n                },\n                {\n                  \"durationMs\": 259200000\n                },\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          },\n          {\n            \"id\": \"b48a696e-cf64-450b-a7cc-9e0a5e457170\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"SelectedSubscriptions\",\n            \"label\": \"Subscription\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') | where ContainerType_s =~ 'microsoft.resources/subscriptions' | project subscriptionId = SubscriptionGuid_g, SubscriptionName = ContainerName_s | distinct subscriptionId, SubscriptionName | order by SubscriptionName asc\",\n            \"value\": [\n              \"value::all\"\n            ],\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            },\n            \"timeContextFromParameter\": \"ResourcesTimeRange\",\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n          }\n        ],\n        \"style\": \"pills\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters - 0\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"818bfe0d-ac41-432d-b11e-132a88c2ee35\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Overview\",\n            \"subTarget\": \"General\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"9fe4860d-95b6-43b8-bded-9502e535d26e\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"VMs\",\n            \"subTarget\": \"VirtualMachines\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"f85c45e2-e0de-4cb1-98e8-7c3300d43bf9\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"VM Disks\",\n            \"subTarget\": \"Disks\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"49f1781d-600e-4740-ba6f-5bb77c3fa510\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"VM NICs\",\n            \"subTarget\": \"NetworkInterfaces\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"b53be37d-ec7d-43a6-880b-ba2744f74f99\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"VM Scale Sets\",\n            \"subTarget\": \"VMSS\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"46b12a25-7de3-4ce2-8168-4ad86c1260da\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"VMSS Disks\",\n            \"subTarget\": \"VMSSDisks\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"6fe330d4-6150-448b-88bd-ed0cfbe5a569\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"VNets\",\n            \"subTarget\": \"VirtualNetworks\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"cc05569b-dfb3-455e-a472-3cd9209bf4ad\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"NSGs\",\n            \"subTarget\": \"NSGs\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"27d46a4c-8485-44e6-971d-35c2091ecf13\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Load Balancers\",\n            \"subTarget\": \"LoadBalancers\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"e0bd5390-0209-4d07-aaae-4aedbe44627a\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Public IPs\",\n            \"subTarget\": \"PublicIPs\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"cdcd8c8c-4024-4266-90a3-3f8d05f51d76\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Application Gateways\",\n            \"subTarget\": \"ApplicationGateways\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"4bc436f7-e363-4bfc-b884-59c27272b9f5\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"App Service Plans\",\n            \"subTarget\": \"AppServicePlans\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"b6def48d-032d-49f7-809a-d3108e1a617a\",\n            \"cellValue\": \"SelectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"SQL Databases\",\n            \"subTarget\": \"SQLDatabases\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"tabs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'VMs' | summarize TileValue=count() by ResourceType\\r\\n| union isfuzzy=true ( AzureOptimizationVMSSV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Scale Sets' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationVMSSV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Scale Set VMs' | summarize TileValue=sum(toint(Capacity_s)) by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationVMSSV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Scale Set Disks' | summarize TileValue=sum(toint(Capacity_s) + toint(Capacity_s) * toint(DataDiskCount_s)) by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationDisksV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Managed Disks' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationVhdDisksV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'VHDs' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationVNetsV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct InstanceId_s | extend ResourceType = 'VNets' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationVNetsV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Subnets' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationNICsV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct InstanceId_s | extend ResourceType = 'NICs' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationLoadBalancersV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Load Balancers' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationAppGatewaysV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'App GWs' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationPublicIPsV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ResourceType = 'Public IPs' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationNSGsV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct InstanceId_s | extend ResourceType = 'NSGs' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationAppServicePlansV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct InstanceId_s | extend ResourceType = 'App Service Plans' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationSqlDbV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct InstanceId_s | extend ResourceType = 'SQL DBs' | summarize TileValue=count() by ResourceType )\\r\\n| union isfuzzy=true ( AzureOptimizationResourceContainersV1_CL | where todatetime(TimeGenerated) > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend ResourceType = 'All Resources' | summarize TileValue=sum(toint(ResourceCount_s)) by ResourceType )\\r\\n| project ResourceType, TileValue\",\n              \"size\": 3,\n              \"title\": \"Total Resources (today)\",\n              \"timeContext\": {\n                \"durationMs\": 86400000\n              },\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"tiles\",\n              \"tileSettings\": {\n                \"titleContent\": {\n                  \"columnMatch\": \"ResourceType\",\n                  \"formatter\": 1\n                },\n                \"leftContent\": {\n                  \"columnMatch\": \"TileValue\",\n                  \"formatter\": 12,\n                  \"formatOptions\": {\n                    \"palette\": \"auto\"\n                  },\n                  \"numberFormat\": {\n                    \"unit\": 0,\n                    \"options\": {\n                      \"style\": \"decimal\"\n                    }\n                  }\n                },\n                \"showBorder\": true,\n                \"sortCriteriaField\": \"TileValue\",\n                \"sortOrderField\": 2,\n                \"size\": \"auto\"\n              }\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"resourcesTiles\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionType = tostring(parse_json(ContainerProperties_s).subscriptionPolicies.quotaId) | summarize count() by SubscriptionType, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Subscriptions by Type\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"SubscriptionsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend ManagementGroup = tostring(parse_json(ContainerProperties_s).managementGroupAncestorsChain[0].displayName) | summarize count() by ManagementGroup, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Subscriptions by Management Group\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"MGsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions/resourcegroups' | project InstanceId_s, SubscriptionGuid_g, todatetime(StatusDate_s) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Resource Groups by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"chartSettings\": {\n                \"customThresholdLine\": \"980\",\n                \"customThresholdLineStyle\": 5\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"rgsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | summarize sum(toint(ResourceCount_s)) by ContainerName_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Resources by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"resourcesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > datetime('{ResourcesTimeRange:startISO}') and Model_s == 'AzureRM' and Scope_s startswith '/subscriptions/' | extend SubscriptionGuid_g = tostring(split(Scope_s, '/')[2]) | where SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(TimeGenerated, 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"RBAC Assignments by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"chartSettings\": {\n                \"customThresholdLine\": \"4000\",\n                \"customThresholdLineStyle\": 5\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"subRbacOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationRBACAssignmentsV1_CL | where TimeGenerated > datetime('{ResourcesTimeRange:startISO}') and Model_s == 'AzureRM' and Scope_s has 'managementGroups' | extend ManagementGroupId = tostring(split(Scope_s, '/')[4]) | summarize count() by ManagementGroupId, bin(TimeGenerated, 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"RBAC Assignments by Management Group\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"chartSettings\": {\n                \"customThresholdLine\": \"500\",\n                \"customThresholdLineStyle\": 5\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"General\"\n            },\n            \"name\": \"mgRbacOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"General\"\n      },\n      \"name\": \"overviewGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"24131817-f7b9-4a2f-9cc9-7deceb0a87c4\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmRegion\",\n                  \"label\": \"Region\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct Location_s | order by Location_s asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmSize\",\n                  \"label\": \"Size\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct VMSize_s | order by VMSize_s asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"id\": \"5d2ec47b-c62e-4ffb-a569-9f0bb3b7f54b\"\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmOsType\",\n                  \"label\": \"OS Type\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct OSType_s | order by OSType_s asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"id\": \"fad59446-6bf3-4b91-abf2-150b79937416\"\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmOsModel\",\n                  \"label\": \"OS Model\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | distinct OSModel | order by OSModel asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"id\": \"f093b915-126e-44ba-b5fa-621ec5ef504b\"\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmImageModel\",\n                  \"label\": \"Image Model\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | distinct ImageModel | order by ImageModel asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"id\": \"ac8bd008-07cc-4ea1-a2c9-52437772db0f\"\n                },\n                {\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmPowerState\",\n                  \"label\": \"Power State\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | distinct PowerState | order by PowerState asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n                  \"id\": \"98e37c72-b912-412e-a3a4-061acadf2d1c\"\n                }\n              ],\n              \"style\": \"pills\",\n              \"doNotRunWhenHidden\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(CoresCount_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Cores by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"coresSubscriptionsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize sum(toint(CoresCount_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Cores by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"coresLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by tolower(VMSize_s), bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Size\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmSizesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where TimeGenerated > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | extend Size = tolower(VMSize_s) | summarize count() by Size | order by count_ \",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Size\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"filter\": true\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmSizesLast\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by OSType_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by OS Type\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmOsTypeOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by PowerState, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Power State\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmPowerStateOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by OSModel, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by OS Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmOSModelOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where TimeGenerated > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by OSModel | order by count_\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by OS Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"filter\": true\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmOSModelLast\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by ImageModel, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Image Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmImageModelOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where TimeGenerated > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | summarize count() by ImageModel | order by count_\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Image Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"filter\": true\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmImageModelLast\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSModel = iif(isnotempty(OSName_s), strcat(OSName_s, \\\"_\\\", OSVersion_s), 'NotAvailable/NotRunning') | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | extend PowerState = iif(isnotempty(PowerState_s), iif(PowerState_s has 'PowerState', tostring(split(PowerState_s, '/')[1]), PowerState_s), 'Unsupported') | where ('{vmRegion:label}' == 'All' or Location_s in ({vmRegion:value})) and ('{vmSize:label}' == 'All' or VMSize_s in ({vmSize:value})) and ('{vmOsType:label}' == 'All' or OSType_s in ({vmOsType:value})) and ('{vmOsModel:label}' == 'All' or OSModel in ({vmOsModel:value})) and ('{vmImageModel:label}' == 'All' or ImageModel in ({vmImageModel:value})) and ('{vmPowerState:label}' == 'All' or PowerState in ({vmPowerState:value})) | extend DiskType = iif(UsesManagedDisks_s == 'true', 'Managed', 'Unmanaged') | summarize count() by DiskType, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VMs by Disk Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualMachines\"\n            },\n            \"name\": \"vmManagedDisksOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"VirtualMachines\"\n      },\n      \"name\": \"vmGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNICsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NICs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NetworkInterfaces\"\n            },\n            \"name\": \"nicsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNICsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, Location_s, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NICs by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NetworkInterfaces\"\n            },\n            \"name\": \"nicsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNICsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s, EnableAcceleratedNetworking_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend AcceleratedNetworking = iif(tobool(EnableAcceleratedNetworking_s), 'Enabled', 'Disabled') | summarize count() by AcceleratedNetworking, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NICs by Accelerated Networking Enabled\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NetworkInterfaces\"\n            },\n            \"name\": \"nicsAccelNetOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNICsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s, PrivateIPAllocationMethod_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by PrivateIPAllocationMethod_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NICs by Allocation Method\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NetworkInterfaces\"\n            },\n            \"name\": \"nicsAllocMethodOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNICsV1_CL | where isnotempty(PublicIPId_s) | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NICs with Public IP by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NetworkInterfaces\"\n            },\n            \"name\": \"nicsPublicIPOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNICsV1_CL | where isempty(OwnerPEId_s) and isempty(OwnerVMId_s) | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unattached NICs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NetworkInterfaces\"\n            },\n            \"name\": \"unusedNICsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"NetworkInterfaces\"\n      },\n      \"name\": \"nicGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"0d1dc8de-e6d4-4a49-835b-4c7695a17252\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"diskSKU\",\n                  \"label\": \"SKU\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | distinct SKU | order by SKU asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                },\n                {\n                  \"id\": \"c2b60b5b-6c48-4b29-9f7d-f586ba5f8dce\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"diskType\",\n                  \"label\": \"Disk Type\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | distinct DiskType | order by DiskType asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                }\n              ],\n              \"style\": \"pills\",\n              \"doNotRunWhenHidden\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"diskParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | summarize count() by SKU, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksSkusOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | summarize count() by DeploymentModel_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksModelOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | summarize count() by DiskType, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Type\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksTypeOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | extend Caching = iif(isnotempty(Caching_s), Caching_s, 'NotAvailable') | summarize count() by Caching, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Caching\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksCachingOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) and isempty(OwnerVMId_s) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) and isempty(OwnerVMId_s) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unattached Disks by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksUnattachedOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationDisksV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | union isfuzzy=true (     AzureOptimizationVhdDisksV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) ) | extend DiskType = iif(isnotempty(DiskType_s), DiskType_s, 'NotAvailable') | extend SKU = tolower(iif(isnotempty(SKU_s), SKU_s, 'Unsupported')) | where ('{diskType:label}' == 'All' or DiskType in ({diskType:value})) and ('{diskSKU:label}' == 'All' or SKU in ({diskSKU:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(todouble(DiskSizeGB_s)/1024) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Total Disks Size (TB) by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"Disks\"\n            },\n            \"name\": \"disksSizeOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"Disks\"\n      },\n      \"name\": \"diskGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 9,\n            \"content\": {\n              \"version\": \"KqlParameterItem/1.0\",\n              \"parameters\": [\n                {\n                  \"id\": \"edb124b8-f4f0-40ca-8139-b2b512f1b9a4\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmssRegion\",\n                  \"label\": \"Region\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct Location_s | order by Location_s asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                },\n                {\n                  \"id\": \"832671d5-9add-4043-8321-5f944cafbe82\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmssSize\",\n                  \"label\": \"Size\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct VMSSSize_s | order by VMSSSize_s asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                },\n                {\n                  \"id\": \"cf1f8d5f-345c-4354-9933-7d77d76b238a\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmssOsType\",\n                  \"label\": \"OS Type\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | distinct OSType_s | order by OSType_s asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                },\n                {\n                  \"id\": \"d88e19c0-bce5-4e92-a789-04a72884f142\",\n                  \"version\": \"KqlParameterItem/1.0\",\n                  \"name\": \"vmssImageModel\",\n                  \"label\": \"Image Model\",\n                  \"type\": 2,\n                  \"multiSelect\": true,\n                  \"quote\": \"'\",\n                  \"delimiter\": \",\",\n                  \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | distinct ImageModel | order by ImageModel asc\",\n                  \"value\": [\n                    \"value::all\"\n                  ],\n                  \"typeSettings\": {\n                    \"additionalResourceOptions\": [\n                      \"value::all\"\n                    ],\n                    \"selectAllValue\": \"*\",\n                    \"showDefault\": false\n                  },\n                  \"timeContext\": {\n                    \"durationMs\": 86400000\n                  },\n                  \"queryType\": 0,\n                  \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n                }\n              ],\n              \"style\": \"pills\",\n              \"doNotRunWhenHidden\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssParameters\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(Capacity_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Set VMs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssVMsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize sum(toint(Capacity_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Set VMs by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssVMsLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(CoresCount_s)*toint(Capacity_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Set Cores by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssCoresSubscriptionsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize sum(toint(CoresCount_s)*toint(Capacity_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Set Cores by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssCoresLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize count() by tolower(VMSSSize_s), bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Size\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssSizesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize sum(toint(Capacity_s)) by tolower(VMSSSize_s), bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Set VMs by Size\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssVMsSizesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where TimeGenerated > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value}))and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | extend Size = tolower(VMSSSize_s) | summarize count() by Size | order by count_ \",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Size\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"filter\": true\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssSizesLast\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where TimeGenerated > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value}))and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | extend Size = tolower(VMSSSize_s) | summarize VMs=sum(toint(Capacity_s)) by Size | order by VMs\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Set VMs by Size\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"filter\": true\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssVMsSizesLast\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value}))and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value}))| summarize count() by OSType_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by OS Type\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssOsTypeOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value})) and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | extend DiskType = iif(UsesManagedDisks_s == 'true', 'Managed', 'Unmanaged') | summarize count() by DiskType, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Disk Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssManagedDisksOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value}))and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize count() by ImageModel, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Image Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssImageModelOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where TimeGenerated > ago(1d) and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend ImageModel = iif(isnotempty(ImageOffer_s), iif(ImageOffer_s startswith '/', strcat(\\\"Custom_\\\", ImageOffer_s), strcat(ImageOffer_s, \\\"_\\\", ImageSku_s)), 'NotAvailable') | where ('{vmssRegion:label}' == 'All' or Location_s in ({vmssRegion:value})) and ('{vmssSize:label}' == 'All' or VMSSSize_s in ({vmssSize:value})) and ('{vmssOsType:label}' == 'All' or OSType_s in ({vmssOsType:value}))and ('{vmssImageModel:label}' == 'All' or ImageModel in ({vmssImageModel:value})) | summarize count() by ImageModel | order by count_\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Scale Sets by Image Model\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"filter\": true\n              }\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSS\"\n            },\n            \"name\": \"vmssImageModelLast\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"VMSS\"\n      },\n      \"name\": \"vmssGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend DiskCount = toint(Capacity_s) + toint(Capacity_s) * toint(DataDiskCount_s) | summarize sum(DiskCount) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSSDisks\"\n            },\n            \"name\": \"vmssDisksOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend DiskCount = toint(Capacity_s) + toint(Capacity_s) * toint(DataDiskCount_s) | summarize sum(DiskCount) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSSDisks\"\n            },\n            \"name\": \"vmssDisksLocationsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend SKU = tolower(iif(isnotempty(OSDiskSKU_s), OSDiskSKU_s, 'Unsupported')) | extend DiskCount = toint(Capacity_s) | summarize sum(DiskCount) by SKU, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"OS Disks by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSSDisks\"\n            },\n            \"name\": \"vmssOsdisksSkusOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | extend OSDiskCount = toint(Capacity_s), DataDiskCount = toint(Capacity_s) * toint(DataDiskCount_s) | summarize OS=sum(OSDiskCount), Data=sum(DataDiskCount) by bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Disks by Type\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSSDisks\"\n            },\n            \"name\": \"vmssDisksTypeOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVMSSV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(todouble(OSDiskSize_s)*todouble(Capacity_s)/1024) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Total OS Disks Size (TB) by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VMSSDisks\"\n            },\n            \"name\": \"vmssDisksSizeOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"VMSSDisks\"\n      },\n      \"name\": \"vmssDisksGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VNets by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"vnetsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Subnets by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"subnetsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, Location_s, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"VNets by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"vnetsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Subnets by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"subnetsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let VNetTotalIPs = AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | summarize SubnetCount=count() by InstanceId_s, VNetPrefixes_s, StatusDate_s | extend AddressSpace = split(VNetPrefixes_s, ' ') | mvexpand AddressSpace | extend TotalIPs = pow(2, 32 - toint(split(tostring(AddressSpace), '/')[1])) | summarize VNetIPs=sum(TotalIPs) by InstanceId_s, SubnetCount, StatusDate_s | extend TotalVNetIPs = VNetIPs - (5*SubnetCount) | summarize by TotalVNetIPs, InstanceId_s, StatusDate_s; AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner ( AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | join kind=inner ( VNetTotalIPs ) on InstanceId_s, StatusDate_s | summarize UsedIPs=sum(toint(SubnetUsedIPs_s)) by InstanceId_s, VNetName_s, TotalVNetIPs, SubscriptionName, StatusDate_s | extend VNet = strcat(VNetName_s, '_', SubscriptionName), AvailableIPs = TotalVNetIPs - UsedIPs | summarize by VNet, AvailableIPs, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Available IPs by VNet\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"vnetsAvailableIPsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let VNetTotalIPs = AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | summarize SubnetCount=count() by InstanceId_s, VNetPrefixes_s, StatusDate_s | extend AddressSpace = split(VNetPrefixes_s, ' ') | mvexpand AddressSpace | extend TotalIPs = pow(2, 32 - toint(split(tostring(AddressSpace), '/')[1])) | summarize VNetIPs=sum(TotalIPs) by InstanceId_s, SubnetCount, StatusDate_s | extend TotalVNetIPs = VNetIPs - (5*SubnetCount) | summarize by TotalVNetIPs, InstanceId_s, StatusDate_s; AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner ( AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | join kind=inner ( VNetTotalIPs ) on InstanceId_s, StatusDate_s | summarize UsedIPs=sum(toint(SubnetUsedIPs_s)) by InstanceId_s, VNetName_s, TotalVNetIPs, SubscriptionName, StatusDate_s | extend VNet = strcat(VNetName_s, '_', SubscriptionName), IPUsagePercentage = todouble(UsedIPs) / todouble(TotalVNetIPs) * 100 | summarize by VNet, IPUsagePercentage, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"IP Usage % by VNet\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"vnetsIPUsageOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner ( AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend Subnet = strcat(VNetName_s, '_', SubnetName_s, '_', SubscriptionName), IPUsagePercentage = todouble(SubnetUsedIPs_s) / todouble(SubnetTotalPrefixIPs_s) * 100 | where IPUsagePercentage > 70 | summarize by Subnet, IPUsagePercentage, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Subnets over 70% IP Usage\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"subnetsIPUsageOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner ( AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | distinct InstanceId_s, VNetName_s, StatusDate_s, SubscriptionName, PeeringsCount_s | extend VNet = strcat(VNetName_s, '_', SubscriptionName), Peerings = toint(PeeringsCount_s) | summarize by VNet, Peerings, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Peerings by VNet\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"vnetsPeeringsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner ( AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize TotalUsedIPs=sum(toint(SubnetUsedIPs_s)) by InstanceId_s, VNetName_s, SubscriptionName, StatusDate_s | where TotalUsedIPs == 0 | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unused VNets by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"unusedVNetsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationVNetsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner ( AzureOptimizationResourceContainersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | where ContainerType_s =~ 'microsoft.resources/subscriptions' | extend SubscriptionName = ContainerName_s | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where SubnetUsedIPs_s == 0 | extend VNet = strcat(VNetName_s, '_', SubscriptionName) | summarize count() by VNet, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unused subnets by VNet\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"VirtualNetworks\"\n            },\n            \"name\": \"unusedSubnetsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"VirtualNetworks\"\n      },\n      \"name\": \"vnetGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNSGsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NSGs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NSGs\"\n            },\n            \"name\": \"nsgsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNSGsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NSG Rules by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NSGs\"\n            },\n            \"name\": \"nsgRulesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNSGsV1_CL | distinct InstanceId_s, SubscriptionGuid_g, Location_s, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NSGs by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NSGs\"\n            },\n            \"name\": \"nsgsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNSGsV1_CL | where toint(SubnetCount_s) > 0 or toint(NicCount_s) > 0 | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s, SubnetCount_s, NicCount_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend UsageMode = iif(toint(SubnetCount_s) > 0, iif(toint(NicCount_s) > 0, 'Subnet+NIC', 'Subnet'), 'NIC') | summarize count() by UsageMode, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"NSGs by Usage Mode\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NSGs\"\n            },\n            \"name\": \"nsgsUsageModeOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationNSGsV1_CL | where toint(SubnetCount_s) == 0 and toint(NicCount_s) == 0 | distinct InstanceId_s, SubscriptionGuid_g, StatusDate_s | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unused NSGs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"NSGs\"\n            },\n            \"name\": \"unusedNSGsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"NSGs\"\n      },\n      \"name\": \"nsgGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationLoadBalancersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Load Balancers by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"LoadBalancers\"\n            },\n            \"name\": \"lbsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationLoadBalancersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Load Balancers by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"LoadBalancers\"\n            },\n            \"name\": \"lbsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationLoadBalancersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend SKU = strcat(SkuName_s,'_',SkuTier_s) | summarize count() by SKU, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Load Balancers by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"LoadBalancers\"\n            },\n            \"name\": \"lbsSKUsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationLoadBalancersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by LbType_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Load Balancers by Type\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"LoadBalancers\"\n            },\n            \"name\": \"lbsTypesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationLoadBalancersV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where (toint(BackendPoolsCount_s) == 0 or ((toint(BackendIPCount_s) == 0 or isempty(BackendIPCount_s)) and (toint(BackendAddressesCount_s) == 0 or isempty(BackendAddressesCount_s)))) and toint(InboundNatPoolsCount_s) == 0 | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unused Load Balancers by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"LoadBalancers\"\n            },\n            \"name\": \"unusedLBsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"LoadBalancers\"\n      },\n      \"name\": \"lbGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationPublicIPsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Public IPs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PublicIPs\"\n            },\n            \"name\": \"publicIPsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationPublicIPsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Public IPs by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PublicIPs\"\n            },\n            \"name\": \"publicIPsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationPublicIPsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | extend SKU = iif(isnotempty(SkuName_s), strcat(SkuTier_s, '_', SkuName_s), 'classic') | summarize count() by SKU, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Public IPs by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PublicIPs\"\n            },\n            \"name\": \"publicIPsSKUsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationPublicIPsV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by AllocationMethod_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Public IPs by Allocation Method\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PublicIPs\"\n            },\n            \"name\": \"publicIPsAllocMethodOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationPublicIPsV1_CL | where isempty(AssociatedResourceId_s) | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unattached Public IPs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"PublicIPs\"\n            },\n            \"name\": \"unusedPublicIPsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"PublicIPs\"\n      },\n      \"name\": \"pipGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Application Gateways by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"appGWsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Application Gateways by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"appGWsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SkuName_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Application Gateways by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"appGWsSKUsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(SkuCapacity_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Application Gateways Capacity by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"appGWsCapacityOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(SkuCapacity_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Application Gateways Capacity by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"appGWsCapacityRegionOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(SkuCapacity_s)) by SkuName_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Application Gateways Capacity by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"appGWsCapacitySKUOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppGatewaysV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where toint(BackendPoolsCount_s) == 0 or ((toint(BackendIPCount_s) == 0 or isempty(BackendIPCount_s)) and (toint(BackendAddressesCount_s) == 0 or isempty(BackendAddressesCount_s))) | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unused Application Gateways by Subscription \",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"conditionalVisibility\": {\n              \"parameterName\": \"SelectedTab\",\n              \"comparison\": \"isEqualTo\",\n              \"value\": \"ApplicationGateways\"\n            },\n            \"name\": \"unusedAppGWsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"ApplicationGateways\"\n      },\n      \"name\": \"appGWGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plans by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plans by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SkuTier_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plans by Tier\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspTiersOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SkuName_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plans by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspSKUsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Kind_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plans by Kind\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspKindsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(SkuCapacity_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plan Capacity by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspCapacityOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(SkuCapacity_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plan Capacity by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspCapacityRegionOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(SkuCapacity_s)) by SkuTier_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plan Capacity by Tier\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspCapacityTierOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(NumberOfSites_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plan Sites by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspSitesOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize sum(toint(NumberOfSites_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"App Service Plan Sites by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"aspSitesRegionOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationAppServicePlansV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where SkuTier_s != 'Free' and toint(NumberOfSites_s) == 0 | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"Unused Paid App Service Plans by Subscription \",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"unusedASPsOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"AppServicePlans\"\n      },\n      \"name\": \"appServicePlansGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Databases by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Databases by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by SkuTier_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Databases by Tier\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlTiersOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by ServiceObjectiveName_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Databases by SKU\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlSKUsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where SkuTier_s in ('Basic','Standard','Premium') | summarize sum(toint(SkuCapacity_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Database (Single) DTUs by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlDTUsOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where SkuTier_s in ('Basic','Standard','Premium') | summarize sum(toint(SkuCapacity_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Database (Single) DTUs by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlDTUsLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where SkuTier_s !in ('Basic','Standard','Premium','DataWarehouse') | summarize sum(toint(SkuCapacity_s)) by SubscriptionName, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Database vCores by Subscription\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlCoresOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | where SkuTier_s !in ('Basic','Standard','Premium','DataWarehouse') | summarize sum(toint(SkuCapacity_s)) by Location_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Database vCores by Region\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlCoresLocationOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationSqlDbV1_CL | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value}) | join kind=inner (     AzureOptimizationResourceContainersV1_CL     | where todatetime(StatusDate_s) > datetime('{ResourcesTimeRange:startISO}') and SubscriptionGuid_g in ({SelectedSubscriptions:value})     | where ContainerType_s =~ 'microsoft.resources/subscriptions'     | extend SubscriptionName = ContainerName_s     | distinct SubscriptionGuid_g, SubscriptionName ) on SubscriptionGuid_g | summarize count() by StorageAccountType_s, bin(todatetime(StatusDate_s), 1d) | render timechart\",\n              \"size\": 1,\n              \"aggregation\": 5,\n              \"showAnalytics\": true,\n              \"title\": \"SQL Databases by Backup Storage Redundancy\",\n              \"timeContextFromParameter\": \"ResourcesTimeRange\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"sqlRedundancyOverTime\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"SelectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"SQLDatabases\"\n      },\n      \"name\": \"sqlGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  },
  {
    "path": "views/workbooks/savingsplans-usage.bicep",
    "content": "@description('The friendly name for the workbook that is used in the Gallery or Saved List.  This name must be unique within a resource group.')\nparam workbookDisplayName string = 'Savings Plans Usage'\n\n@description('The gallery that the workbook will been shown under. Supported values include workbook, tsg, etc. Usually, this is \\'workbook\\'')\nparam workbookType string = 'workbook'\n\n@description('The id of resource instance to which the workbook will be associated')\nparam workbookSourceId string\n\n@description('The unique guid for this workbook instance')\nparam workbookId string = 'a4a4bb1e-0a20-45b8-ab47-4bc38f9cc22e'\nparam resourceTags object\n\nparam resourceGroupLocation string = resourceGroup().location\n\nresource workbookId_resource 'microsoft.insights/workbooks@2022-04-01' = {\n  name: workbookId\n  location: resourceGroupLocation\n  tags: resourceTags\n  kind: 'shared'\n  properties: {\n    displayName: workbookDisplayName\n    serializedData: string(loadJsonContent('savingsplans-usage.json'))\n    version: '1.0'\n    sourceId: workbookSourceId\n    category: workbookType\n  }\n  dependsOn: []\n}\n\noutput workbookId string = workbookId_resource.id\n"
  },
  {
    "path": "views/workbooks/savingsplans-usage.json",
    "content": "{\n  \"version\": \"Notebook/1.0\",\n  \"items\": [\n    {\n      \"type\": 9,\n      \"content\": {\n        \"version\": \"KqlParameterItem/1.0\",\n        \"parameters\": [\n          {\n            \"id\": \"b58b4eb8-5821-44d2-bc7e-54054df27320\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"LookbackPeriod\",\n            \"label\": \"Lookback Period\",\n            \"type\": 4,\n            \"isRequired\": true,\n            \"value\": {\n              \"durationMs\": 2592000000\n            },\n            \"typeSettings\": {\n              \"selectableValues\": [\n                {\n                  \"durationMs\": 604800000\n                },\n                {\n                  \"durationMs\": 1209600000\n                },\n                {\n                  \"durationMs\": 2592000000\n                },\n                {\n                  \"durationMs\": 5184000000\n                },\n                {\n                  \"durationMs\": 7776000000\n                }\n              ],\n              \"allowCustom\": true\n            },\n            \"timeContext\": {\n              \"durationMs\": 86400000\n            }\n          },\n          {\n            \"id\": \"5b2d78e9-7177-4d9b-86fa-2a9b12dd470a\",\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"SavingsPlan\",\n            \"label\": \"Savings Plan\",\n            \"type\": 2,\n            \"isRequired\": true,\n            \"multiSelect\": true,\n            \"quote\": \"'\",\n            \"delimiter\": \",\",\n            \"query\": \"AzureOptimizationSavingsPlansUsageV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ProvisioningState_s == 'Succeeded'\\r\\n| distinct SavingsPlanId_g, DisplayName_s\\r\\n| order by DisplayName_s asc\",\n            \"typeSettings\": {\n              \"additionalResourceOptions\": [\n                \"value::all\"\n              ],\n              \"showDefault\": false\n            },\n            \"timeContext\": {\n              \"durationMs\": 0\n            },\n            \"timeContextFromParameter\": \"LookbackPeriod\",\n            \"queryType\": 0,\n            \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n            \"value\": [\n              \"value::all\"\n            ]\n          },\n          {\n            \"version\": \"KqlParameterItem/1.0\",\n            \"name\": \"Aggregator\",\n            \"label\": \"Aggregator Tag\",\n            \"type\": 1,\n            \"isRequired\": true,\n            \"timeContext\": {\n              \"durationMs\": 2592000000\n            },\n            \"id\": \"a3fb4877-28ef-43fc-8821-376df486fa2a\",\n            \"value\": null\n          }\n        ],\n        \"style\": \"above\",\n        \"queryType\": 0,\n        \"resourceType\": \"microsoft.operationalinsights/workspaces\"\n      },\n      \"name\": \"parameters\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"Consumption data is updated once every 24 hours and is presented in the currency of your Azure consumption agreement.\",\n        \"style\": \"info\"\n      },\n      \"name\": \"text - 7\"\n    },\n    {\n      \"type\": 1,\n      \"content\": {\n        \"json\": \"If below tiles are reporting query errors, you must set up Pricesheet exports. See more details [here](https://aka.ms/AzureOptimizationEngine/commitmentssetup).\",\n        \"style\": \"warning\"\n      },\n      \"name\": \"text - 10\"\n    },\n    {\n      \"type\": 11,\n      \"content\": {\n        \"version\": \"LinkItem/1.0\",\n        \"style\": \"tabs\",\n        \"links\": [\n          {\n            \"id\": \"93e7a6c7-cb1f-49ee-b135-468b9f528b04\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Savings Plans Usage Analysis\",\n            \"subTarget\": \"savingsPlansAnalysis\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"b9dd48c2-3dce-4977-af3e-44df1d8758b7\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Usage by Tag\",\n            \"subTarget\": \"usageByTag\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"96332944-d3f7-4b0b-ad56-ab25a9e91049\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Full Usage Report\",\n            \"subTarget\": \"fullReport\",\n            \"style\": \"link\"\n          },\n          {\n            \"id\": \"1f438af9-e6ff-470e-9b11-b1b5a701e51b\",\n            \"cellValue\": \"selectedTab\",\n            \"linkTarget\": \"parameter\",\n            \"linkLabel\": \"Unused Savings Plans Analysis\",\n            \"subTarget\": \"unusedSavingsPlans\",\n            \"style\": \"link\"\n          }\n        ]\n      },\n      \"name\": \"tabs\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandUnitPrice;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\\r\\n| distinct MeterID_g, OnDemandUnitPrice\\r\\n| union (VMOnDemandPriceSheet)\\r\\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId in ({SavingsPlan:value})\\r\\n| join kind=leftouter (OnDemandPriceSheet) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend DiscountPercent = (1 - todouble(EffectivePrice_s)/OnDemandUnitPrice) * 100\\r\\n| summarize UsedSPsHourly=round(sum(todouble(CostInBillingCurrency_s)/24),2), DiscountPercent=round(avg(DiscountPercent),2) by Date_s, savingsPlanId\\r\\n| summarize UsedSPsHourly=round(avg(UsedSPsHourly),2), DiscountPercent=round(avg(DiscountPercent),2) by savingsPlanId\\r\\n| join kind=rightouter (\\r\\n    AzureOptimizationSavingsPlansUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where SavingsPlanId_g in~ ({SavingsPlan:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by SavingsPlanId_g\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | project savingsPlanId=SavingsPlanId_g, benefitName_s=DisplayName_s, CommitmentAmount_s, CommitmentGrain_s, CommitmentCurrencyCode_s, Term_s, Util7Days_s=round(todouble(Util7Days_s)), Util30Days_s=round(todouble(Util30Days_s))\\r\\n) on savingsPlanId\\r\\n| project savingsPlanId, benefitName_s, CommitmentAmount_s, UsedSPsHourly=iif(isempty(UsedSPsHourly),0.0,UsedSPsHourly), CommitmentCurrencyCode_s, CommitmentGrain_s, Term_s, Util7Days_s, Util30Days_s, DiscountPercent\",\n              \"size\": 0,\n              \"title\": \"Savings Plan Usage Details\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"savingsPlanId\",\n              \"exportParameterName\": \"selectedSavingsPlan\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"savingsPlanId\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"benefitName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"24ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"CommitmentAmount_s\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UsedSPsHourly\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"CommitmentCurrencyCode_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"CommitmentGrain_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"10ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"14ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"DiscountPercent\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"13ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"benefitId_s\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"savingsPlanId\",\n                    \"label\": \"Savings Plan ID\"\n                  },\n                  {\n                    \"columnId\": \"benefitName_s\",\n                    \"label\": \"Savings Plan\"\n                  },\n                  {\n                    \"columnId\": \"CommitmentAmount_s\",\n                    \"label\": \"Commited\"\n                  },\n                  {\n                    \"columnId\": \"UsedSPsHourly\",\n                    \"label\": \"Used (Avg.)\"\n                  },\n                  {\n                    \"columnId\": \"CommitmentCurrencyCode_s\",\n                    \"label\": \"Currency\"\n                  },\n                  {\n                    \"columnId\": \"CommitmentGrain_s\",\n                    \"label\": \"Grain\"\n                  },\n                  {\n                    \"columnId\": \"Term_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"DiscountPercent\",\n                    \"label\": \"Discount\"\n                  }\n                ]\n              },\n              \"sortBy\": []\n            },\n            \"customWidth\": \"55\",\n            \"name\": \"spUsageDetailsV2\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId == '{selectedSavingsPlan:value}'\\r\\n| extend VMSize = tostring(parse_json(AdditionalInfo_s).ServiceType)\\r\\n| extend ConsumedSize = iif(isnotempty(VMSize), VMSize, strcat(MeterSubCategory_s, ' ', MeterName_s))\\r\\n| summarize round(sum(todouble(CostInBillingCurrency_s)/24),1) by todatetime(Date_s), ConsumedSize\",\n              \"size\": 0,\n              \"aggregation\": 3,\n              \"title\": \"Avg. Savings Plan Hourly Usage by SKU (click on a line in the table at the left)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"ReservationId_g\",\n              \"exportParameterName\": \"selectedReservation\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 1000\n              },\n              \"chartSettings\": {\n                \"group\": \"ConsumedSize\",\n                \"createOtherGroup\": null,\n                \"customThresholdLine\": \"{selectedQuantity}\",\n                \"customThresholdLineStyle\": 1\n              }\n            },\n            \"customWidth\": \"45\",\n            \"name\": \"spUsageDailyAverageBySize\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let absoluteSPUsage = toscalar(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\\r\\n    and ChargeType_s == 'Usage'\\r\\n    and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId == '{selectedSavingsPlan:value}'\\r\\n| summarize sum(todouble(CostInBillingCurrency_s)));\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId == '{selectedSavingsPlan:value}'\\r\\n| summarize UsedSPPercentage=round(sum(todouble(CostInBillingCurrency_s)) / absoluteSPUsage * 100, 2) by ResourceId, SubscriptionId, ResourceLocation_s\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder ResourceId, ResourceLocation_s, Subscription, UsedSPPercentage\\r\\n| order by ResourceId asc, UsedSPPercentage\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Savings Plan Usage by Resource (click on a line in the table above)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"benefitId_s\",\n              \"exportParameterName\": \"selectedSavingsPlan\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"ResourceId\",\n                    \"label\": \"Resource ID\"\n                  },\n                  {\n                    \"columnId\": \"ResourceLocation_s\",\n                    \"label\": \"Location\"\n                  },\n                  {\n                    \"columnId\": \"Subscription\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"UsedSPPercentage\",\n                    \"label\": \"Savings Plan Usage %\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"spUsageDailyAverageByResource\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let absoluteSPUsage = toscalar(AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}')\\r\\n    and TimeGenerated < todatetime('{LookbackPeriod:endISO}')\\r\\n    and ChargeType_s == 'Usage'\\r\\n    and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId == '{selectedSavingsPlan:value}'\\r\\n| summarize sum(todouble(CostInBillingCurrency_s)));\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId == '{selectedSavingsPlan:value}'\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\\r\\n| summarize UsedSPPercentage=round(sum(todouble(CostInBillingCurrency_s)) / absoluteSPUsage * 100, 2) by AggregatorTag, ResourceLocation_s, SubscriptionId\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| project-reorder AggregatorTag, ResourceLocation_s, Subscription, UsedSPPercentage\\r\\n| order by AggregatorTag asc, UsedSPPercentage\",\n              \"size\": 0,\n              \"showAnalytics\": true,\n              \"title\": \"Savings Plan Usage by Tag (click on a line in the table above)\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"exportFieldName\": \"benefitId_s\",\n              \"exportParameterName\": \"selectedSavingsPlan\",\n              \"showExportToExcel\": true,\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  }\n                ],\n                \"rowLimit\": 5000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"ResourceLocation_s\",\n                    \"label\": \"Location\"\n                  },\n                  {\n                    \"columnId\": \"Subscription\",\n                    \"label\": \"Subscription\"\n                  },\n                  {\n                    \"columnId\": \"UsedSPPercentage\",\n                    \"label\": \"Savings Plan Usage %\"\n                  }\n                ]\n              }\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"spUsageDailyAverageByInstance\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"savingsPlansAnalysis\"\n      },\n      \"name\": \"savingsPlanAnalysisGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let UsageBySavingsPlan = AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId in ({SavingsPlan:value})\\r\\n| summarize TotalSPUsage=sum(todouble(CostInBillingCurrency_s)) by benefitId_s;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId in ({SavingsPlan:value})\\r\\n| extend Tags_s = iif(Tags_s startswith \\\"{\\\", Tags_s, strcat(\\\"{\\\", Tags_s, \\\"}\\\"))\\r\\n| extend AggregatorTag = tostring(parse_json(Tags_s)['{Aggregator}'])\\r\\n| join kind=inner (UsageBySavingsPlan) on benefitId_s\\r\\n| summarize UsedSP=round(sum(todouble(CostInBillingCurrency_s)), 2) by benefitName_s, AggregatorTag, SubscriptionId, TotalSPUsage\\r\\n| join kind=leftouter ( \\r\\n    AzureOptimizationResourceContainersV1_CL \\r\\n    | where TimeGenerated > ago(1d) and ContainerType_s == 'microsoft.resources/subscriptions'\\r\\n    | project Subscription=ContainerName_s, SubscriptionId=SubscriptionGuid_g\\r\\n) on SubscriptionId\\r\\n| project-away SubscriptionId*\\r\\n| extend UsedSPPercentage = UsedSP / TotalSPUsage * 100\\r\\n| project benefitName_s, AggregatorTag, Subscription, UsedSPPercentage\\r\\n| order by AggregatorTag asc, UsedSPPercentage\",\n              \"size\": 2,\n              \"exportFieldName\": \"ReservationName_s\",\n              \"exportParameterName\": \"selectedReservationName\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"UsedSPPercentage\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalUsedRIs\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"DaysSeen\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g1\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"UsedRIPercentage\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"benefitName_s\",\n                    \"label\": \"Savings Plan\"\n                  },\n                  {\n                    \"columnId\": \"AggregatorTag\",\n                    \"label\": \"Aggregator Tag\"\n                  },\n                  {\n                    \"columnId\": \"UsedSPPercentage\",\n                    \"label\": \"Savings Plan Usage\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"spUsageByTagQuery\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"usageByTag\"\n      },\n      \"name\": \"usageByTagGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"let LinuxOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption' and MeterSubCategory_s !endswith \\\"Windows\\\"\\r\\n| extend MeterSubCategory_s = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Linux'))\\r\\n| summarize LinuxUnitPrice=max(todouble(UnitPrice_s)) by LinuxMeterId=MeterID_g, MeterName_s, MeterSubCategory_s, MeterRegion_s, LinuxUnitOfMeasure=UnitOfMeasure_s;\\r\\nlet VMOnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s == 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| extend NonWindowsMeterSubcategory = substring(MeterSubCategory_s, 0, indexof(MeterSubCategory_s, ' Windows'))\\r\\n| extend WindowsMeterSubCategory = MeterSubCategory_s\\r\\n| extend NonWindowsMeterSubcategory = substring(NonWindowsMeterSubcategory, 0, indexof(NonWindowsMeterSubcategory, ' Linux'))\\r\\n| summarize UnitPrice_s=max(todouble(UnitPrice_s)) by MeterID_g, MeterName_s, NonWindowsMeterSubcategory, WindowsMeterSubCategory, MeterRegion_s, UnitOfMeasure_s\\r\\n| join kind=leftouter ( LinuxOnDemandPriceSheet ) on MeterName_s, MeterRegion_s, $left.NonWindowsMeterSubcategory == $right.MeterSubCategory_s\\r\\n| extend PricesheetPrice = iif(isnotempty(LinuxUnitPrice), LinuxUnitPrice, UnitPrice_s)\\r\\n| extend PricesheetUnitOfMeasure = iif(isnotempty(LinuxUnitOfMeasure), LinuxUnitOfMeasure, UnitOfMeasure_s)\\r\\n| extend UnitHrs = toint(substring(PricesheetUnitOfMeasure, 0, indexof(PricesheetUnitOfMeasure, 'Hour')-1))\\r\\n| extend OnDemandUnitPrice = PricesheetPrice/UnitHrs\\r\\n| distinct MeterID_g, OnDemandUnitPrice;\\r\\nlet OnDemandPriceSheet = AzureOptimizationPricesheetV1_CL\\r\\n| where TimeGenerated > ago(14d)\\r\\n| where MeterCategory_s != 'Virtual Machines' and PriceType_s == 'Consumption'\\r\\n| summarize OnDemandUnitPrice = max(todouble(UnitPrice_s)) by MeterID_g\\r\\n| distinct MeterID_g, OnDemandUnitPrice\\r\\n| union (VMOnDemandPriceSheet)\\r\\n| summarize OnDemandUnitPrice=min(OnDemandUnitPrice) by MeterID_g;\\r\\nAzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'Usage' and PricingModel_s == 'SavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId in ({SavingsPlan:value})\\r\\n| join kind=leftouter (OnDemandPriceSheet) on $left.MeterId_g == $right.MeterID_g\\r\\n| extend DiscountPercent = (1 - todouble(EffectivePrice_s)/OnDemandUnitPrice) * 100\\r\\n| summarize UsedSPsHourly=round(sum(todouble(CostInBillingCurrency_s)/24),2), DiscountPercent=round(avg(DiscountPercent),2) by Date_s, savingsPlanId\\r\\n| summarize UsedSPsHourly=round(avg(UsedSPsHourly),2), DiscountPercent=round(avg(DiscountPercent),2) by savingsPlanId\\r\\n| join kind=rightouter (\\r\\n    AzureOptimizationSavingsPlansUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where SavingsPlanId_g in~ ({SavingsPlan:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by SavingsPlanId_g\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | project savingsPlanId=SavingsPlanId_g, benefitName_s=DisplayName_s, CommitmentAmount_s, CommitmentGrain_s, CommitmentCurrencyCode_s, Term_s, AppliedScopeType_s, Util7Days_s=round(todouble(Util7Days_s)), Util30Days_s=round(todouble(Util30Days_s)), ExpiryDate_t\\r\\n) on savingsPlanId\\r\\n| extend HoursUntilExpiry=(ExpiryDate_t-now())/1h\\r\\n| extend AmountRemainingToConsume = round(todouble(CommitmentAmount_s) * HoursUntilExpiry)\\r\\n| project savingsPlanId, benefitName_s, CommitmentAmount_s, UsedSPsHourly=iif(isempty(UsedSPsHourly),0.0,UsedSPsHourly), CommitmentCurrencyCode_s, CommitmentGrain_s, Term_s, AppliedScopeType_s, ExpiryDate_t, AmountRemainingToConsume, Util7Days_s, Util30Days_s, DiscountPercent, SavingsMargin=round(todouble(Util7Days_s))-100.0+DiscountPercent \",\n              \"size\": 2,\n              \"exportedParameters\": [\n                {\n                  \"fieldName\": \"ReservationId_g\",\n                  \"parameterName\": \"selectedReservation\"\n                },\n                {\n                  \"fieldName\": \"ReservationName_s\",\n                  \"parameterName\": \"selectedReservationName\",\n                  \"parameterType\": 1\n                }\n              ],\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"savingsPlanId\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"CommitmentAmount_s\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UsedSPsHourly\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ExpiryDate_t\",\n                    \"formatter\": 6,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"15ch\"\n                    },\n                    \"dateFormat\": {\n                      \"showUtcTime\": true,\n                      \"formatName\": \"shortDatePattern\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"AmountRemainingToConsume\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"20ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"minimumFractionDigits\": 2,\n                        \"maximumFractionDigits\": 2\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util7Days_s\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"Util30Days_s\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\"\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"DiscountPercent\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"SavingsMargin\",\n                    \"formatter\": 18,\n                    \"formatOptions\": {\n                      \"thresholdsOptions\": \"colors\",\n                      \"thresholdsGrid\": [\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"5\",\n                          \"representation\": \"yellow\"\n                        },\n                        {\n                          \"operator\": \"<\",\n                          \"thresholdValue\": \"0\",\n                          \"representation\": \"redBright\",\n                          \"text\": \"{0}{1}\"\n                        },\n                        {\n                          \"operator\": \"Default\",\n                          \"thresholdValue\": null,\n                          \"representation\": \"green\",\n                          \"text\": \"{0}{1}\"\n                        }\n                      ]\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"UsedSPPercentage\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalUsedRIs\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"DaysSeen\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"ReservationId_g1\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"TotalReservedQuantity_s\",\n                    \"formatter\": 5\n                  },\n                  {\n                    \"columnMatch\": \"UsedRIPercentage\",\n                    \"formatter\": 0,\n                    \"numberFormat\": {\n                      \"unit\": 1,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"maximumFractionDigits\": 1\n                      }\n                    }\n                  }\n                ],\n                \"rowLimit\": 10000,\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"savingsPlanId\",\n                    \"label\": \"Savings Plan ID\"\n                  },\n                  {\n                    \"columnId\": \"benefitName_s\",\n                    \"label\": \"Savings Plan\"\n                  },\n                  {\n                    \"columnId\": \"CommitmentAmount_s\",\n                    \"label\": \"Committed\"\n                  },\n                  {\n                    \"columnId\": \"UsedSPsHourly\",\n                    \"label\": \"Used (Avg.)\"\n                  },\n                  {\n                    \"columnId\": \"CommitmentCurrencyCode_s\",\n                    \"label\": \"Currency\"\n                  },\n                  {\n                    \"columnId\": \"CommitmentGrain_s\",\n                    \"label\": \"Grain\"\n                  },\n                  {\n                    \"columnId\": \"Term_s\",\n                    \"label\": \"Term\"\n                  },\n                  {\n                    \"columnId\": \"AppliedScopeType_s\",\n                    \"label\": \"Scope\"\n                  },\n                  {\n                    \"columnId\": \"ExpiryDate_t\",\n                    \"label\": \"Expires On\"\n                  },\n                  {\n                    \"columnId\": \"AmountRemainingToConsume\",\n                    \"label\": \"Remain. Commit.\"\n                  },\n                  {\n                    \"columnId\": \"Util7Days_s\",\n                    \"label\": \"Used (7d)\"\n                  },\n                  {\n                    \"columnId\": \"Util30Days_s\",\n                    \"label\": \"Used (30d)\"\n                  },\n                  {\n                    \"columnId\": \"DiscountPercent\",\n                    \"label\": \"Discount\"\n                  },\n                  {\n                    \"columnId\": \"SavingsMargin\",\n                    \"label\": \"Savings\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"spUsageFullReport\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"fullReport\"\n      },\n      \"name\": \"fullReportGroup\"\n    },\n    {\n      \"type\": 12,\n      \"content\": {\n        \"version\": \"NotebookGroup/1.0\",\n        \"groupType\": \"editable\",\n        \"items\": [\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedSavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId in ({SavingsPlan:value})\\r\\n| where todouble(CostInBillingCurrency_s) > 0\\r\\n| extend UnusedCost = todouble(CostInBillingCurrency_s)\\r\\n| project todatetime(Date_s), benefitName_s, UnusedCost\",\n              \"size\": 1,\n              \"title\": \"Cost of Unused Savings Plans over time\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"visualization\": \"barchart\"\n            },\n            \"customWidth\": \"50\",\n            \"name\": \"unusedSavingsPlansOverTime\"\n          },\n          {\n            \"type\": 3,\n            \"content\": {\n              \"version\": \"KqlItem/1.0\",\n              \"query\": \"AzureOptimizationConsumptionV1_CL\\r\\n| where TimeGenerated > todatetime('{LookbackPeriod:startISO}') and TimeGenerated < todatetime('{LookbackPeriod:endISO}') and ChargeType_s == 'UnusedSavingsPlan'\\r\\n| extend savingsPlanId = tostring(split(benefitId_s,'/')[-1])\\r\\n| where savingsPlanId in ({SavingsPlan:value})\\r\\n| summarize TotalUnusedCost = sum(todouble(CostInBillingCurrency_s)) by benefitName_s, savingsPlanId\\r\\n| where round(TotalUnusedCost) > 0\\r\\n| join kind=inner (\\r\\n    AzureOptimizationSavingsPlansUsageV1_CL\\r\\n    | where TimeGenerated > ago(1d)\\r\\n    | where SavingsPlanId_g in~ ({SavingsPlan:value})\\r\\n    | summarize arg_max(TimeGenerated, *) by SavingsPlanId_g\\r\\n    | where ProvisioningState_s == 'Succeeded'\\r\\n    | extend UnusedQuantity = todouble(CommitmentAmount_s) - (todouble(CommitmentAmount_s) * todouble(Util7Days_s) / 100)\\r\\n    | extend UnusedQuantity30d = todouble(CommitmentAmount_s) - (todouble(CommitmentAmount_s) * todouble(Util30Days_s) / 100)\\r\\n    | project savingsPlanId=SavingsPlanId_g, CommitmentAmount_s, CommitmentGrain_s, CommitmentCurrencyCode_s, Term_s, Util7Days_s=round(todouble(Util7Days_s)), UnusedQuantity, Util30Days_s=round(todouble(Util30Days_s)), UnusedQuantity30d, ExpiryDate_t\\r\\n) on savingsPlanId\\r\\n| project-reorder benefitName_s, TotalUnusedCost\\r\\n| order by TotalUnusedCost\\r\\n\",\n              \"size\": 2,\n              \"title\": \"Unused Savings Plans Details\",\n              \"timeContextFromParameter\": \"LookbackPeriod\",\n              \"queryType\": 0,\n              \"resourceType\": \"microsoft.operationalinsights/workspaces\",\n              \"gridSettings\": {\n                \"formatters\": [\n                  {\n                    \"columnMatch\": \"benefitName_s\",\n                    \"formatter\": 0,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"40ch\"\n                    }\n                  },\n                  {\n                    \"columnMatch\": \"TotalUnusedCost\",\n                    \"formatter\": 1,\n                    \"formatOptions\": {\n                      \"customColumnWidthSetting\": \"22ch\"\n                    },\n                    \"numberFormat\": {\n                      \"unit\": 0,\n                      \"options\": {\n                        \"style\": \"decimal\",\n                        \"useGrouping\": true,\n                        \"maximumFractionDigits\": 0\n                      }\n                    }\n                  }\n                ],\n                \"labelSettings\": [\n                  {\n                    \"columnId\": \"benefitName_s\",\n                    \"label\": \"Savings Plan\"\n                  },\n                  {\n                    \"columnId\": \"TotalUnusedCost\",\n                    \"label\": \"Total Unused Cost\"\n                  }\n                ]\n              }\n            },\n            \"name\": \"unusedSavingsPlansDetails\"\n          }\n        ]\n      },\n      \"conditionalVisibility\": {\n        \"parameterName\": \"selectedTab\",\n        \"comparison\": \"isEqualTo\",\n        \"value\": \"unusedSavingsPlans\"\n      },\n      \"name\": \"unusedSavingsPlansGroup\"\n    }\n  ],\n  \"$schema\": \"https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json\"\n}"
  }
]