Azure Cost Analysis – Costs per VM

This Azure Script Based Action allows you to to retrieve detailed information on actual costs of selected VM in the Azure Subscription your Service Principal has access to.
README: https://support.controlup.com/hc/en-us/articles/360011378518#h_01EE8GMB35X9Y20MXV22N1GGE9
Version 1.1.2
Created on 2020-12-25
Modified on 2020-12-27
Created by Esther Barthel, MSc
Downloads: 72

The Script Copy Script Copied to clipboard
<#
.SYNOPSIS
    Get Azure Cost Analysis information, sorted by Service, grouped by Resource.
.DESCRIPTION
    Get Azure Cost Analysis information, using REST API calls.
.EXAMPLE
    Get-AzCostAnalysis
.EXAMPLE
    Get-AzCostAnalysis -SubscriptionID <string>
.CONTEXT
    Windows Virtual Desktops
.NOTES
    Version:        0.1
    Author:         Esther Barthel, MSc
    Creation Date:  2020-10-25
    Updated:        2020-10-25

    Purpose:        WVD Administration, through REST API calls
        
    Copyright (c) cognition IT. All rights reserved.
#>
[CmdletBinding()]
Param(
    [Parameter(
        Position=0, 
        Mandatory=$false, 
        HelpMessage='ControlUp SBA parameter auto entry: Session Host Name'
    )]
    [string] $vmName
)    

If ([string]::IsNullOrEmpty($vmName))
{
    $vmName = ""
}

# ------------------------------------
# | WVD Functions for REST API calls |
# ------------------------------------

#region Global Variables
$wvdApiVersion = "2019-12-10-preview"
#endregion Global Variables

#region Windows Presentation Foundation (WPF) form to store WVD Service Principal information
[string]$mainformXAML = @'
<Window x:Class="wvdSP_Input_Form.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Esther_s_Input_Form"
        mc:Ignorable="d"
        Title="Enter the WVD Service Principal (SP) details" Height="389.336" Width="617.103">
    <Grid>
        <TextBox x:Name="textboxTenantId" HorizontalAlignment="Left" Height="31" Margin="176,50,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="398"/>
        <Label Content="SP Tenant ID" HorizontalAlignment="Left" Height="30" Margin="29,51,0,0" VerticalAlignment="Top" Width="117"/>
        <TextBox x:Name="textboxAppId" HorizontalAlignment="Left" Height="30" Margin="176,118,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="398"/>
        <Label Content="SP App ID" HorizontalAlignment="Left" Height="30" Margin="29,118,0,0" VerticalAlignment="Top" Width="117"/>
        <PasswordBox x:Name="textboxAppSecret" HorizontalAlignment="Left" Height="31" Margin="176,192,0,0" VerticalAlignment="Top" Width="398"/>
        <Label Content="SP App Secret" HorizontalAlignment="Left" Height="30" Margin="29,193,0,0" VerticalAlignment="Top" Width="117"/>
        <Button x:Name="buttonOK" Content="OK" HorizontalAlignment="Left" Height="46" Margin="29,274,0,0" VerticalAlignment="Top" Width="175" IsDefault="True"/>
        <Button x:Name="buttonCancel" Content="Cancel" HorizontalAlignment="Left" Height="46" Margin="244,274,0,0" VerticalAlignment="Top" Width="175" IsDefault="True"/>

    </Grid>
</Window>
'@

function Invoke-WVDSPCredentialsForm {
# Created by Guy Leech - @guyrleech 17/05/2020
    Param
    (
        [Parameter(Mandatory=$true)]
        $inputXaml
    )

    $form = $null
    $inputXML = $inputXaml -replace 'mc:Ignorable="d"' , '' -replace 'x:N' ,'N'  -replace '^<Win.*' , '<Window'
    [xml]$xaml = $inputXML

    if( $xaml )
    {
        $reader = New-Object -TypeName Xml.XmlNodeReader -ArgumentList $xaml

        try
        {
            $form = [Windows.Markup.XamlReader]::Load( $reader )
        }
        catch
        {
            Throw "Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .NET is installed.`n$_"
        }

        $xaml.SelectNodes( '//*[@Name]' ) | ForEach-Object `
        {
            Set-Variable -Name "WPF$($_.Name)" -Value $Form.FindName($_.Name) -Scope Global
        }
    }
    else
    {
        Throw "Failed to convert input XAML to WPF XML"
    }

    $form
}
#endregion WPF form

function Get-AzSPStoredCredentials {
    <#
    .SYNOPSIS
        Retrieve the Azure Service Principal Stored Credentials.
    .DESCRIPTION
        Retrieve the Azure Service Principal Stored Credentials from a stored credentials file.
    .EXAMPLE
        Get-AzSPStoredCredentials
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-08-03
        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param()

    #region function settings
        # Stored Credentials XML file
        $System = "AZ"
        $strAzSPCredFolder = "$([environment]::GetFolderPath('CommonApplicationData'))\ControlUp\ScriptSupport"
        $AzSPCredentials = $null
    #endregion

    Write-Verbose ""
    Write-Verbose "----------------------------- "
    Write-Verbose "| Get Azure SP Credentials: | "
    Write-Verbose "----------------------------- "
    Write-Verbose ""

    If (Test-Path -Path "$($strAzSPCredFolder)\$($env:USERNAME)_$($System)_Cred.xml")
    {
        try 
        {
            $AzSPCredentials = Import-Clixml -Path "$strAzSPCredFolder\$($env:USERNAME)_$($System)_Cred.xml"
        }
        catch 
        {
            Write-Error ("The required PSCredential object could not be loaded. " + $_)
        }
    }
    Else
    {
        Write-Error "The Azure Service Principal Credentials file stored for this user ($($env:USERNAME)) cannot be found. `nCreate the file with the Set-AzSPCredentials script action (prerequisite)."
        Exit
    }
    return $AzSPCredentials
}

function Get-AzBearerToken {
    <#
    .SYNOPSIS
        Retrieve the Azure Bearer Token for an authentication session.
    .DESCRIPTION
        Retrieve the Azure Bearer Token for an authentication session, using a REST API call.
    .EXAMPLE
        Get-AzBearerToken -SPCredentials <PSCredentialObject> -TenantID <string>
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-03-22
        Updated:        2020-05-08
                        Created a separate Azure Credentials function to support ARM architecture and REST API scripted actions
        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            HelpMessage='Enter the Service Principal credentials'
        )]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.PSCredential] $SPCredentials,

        [Parameter(
            Position=1, 
            Mandatory=$true, 
            HelpMessage='Enter the Tenant ID'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $TenantID
    )

    #region Prep variables
        # URL for REST API call to authenticate with Azure (using the TenantID parameter)
        $uri = "https://login.microsoftonline.com/$TenantID/oauth2/token"
        
        # Create the Invoke-RestMethod Body (using the SPCredentials parameter)
        $body = @{
            grant_type="client_credentials";
            client_Id=$($SPCredentials.UserName);
            client_Secret=$($SPCredentials.GetNetworkCredential().Password);
            resource="https://management.azure.com"
        }
        #debug: $body

        # Create the Invoke-RestMethod parameters
        $invokeRestMethodParams = @{
            Uri             = $uri
            Body            = $body
            Method          = "POST"
            ContentType     = "application/x-www-form-urlencoded"
        }
    #endregion

    try 
    {
        $response = $null
        # Make the REST API call with the created parameters
        $response = Invoke-RestMethod @invokeRestMethodParams
    }
    catch 
    {
        Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
    }
    # return the JSON response
    return $response
}

function Get-AzSubscription {
    <#
    .SYNOPSIS
        Retrieve the Azure Subscription information.
    .DESCRIPTION
        Retrieve the Azure Subscription information, using a REST API call.
    .EXAMPLE
        Get-AzSubscription -BearerToken <string> -SubscriptionID <string>
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-09-20
        Updated:        2020-09-20
                        Created a separate Azure Credentials function to support ARM architecture and REST API scripted actions

        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            HelpMessage='Enter a valid bearer token'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $BearerToken,

        [Parameter(
            Position=1, 
            Mandatory=$false, 
            HelpMessage='Enter a subscriptionID'
        )]
        [string] $SubscriptionID
    )

    #region Prep variables
        # URL for REST API call to authenticate with Azure (using the TenantID parameter)
        $uri = "https://management.azure.com/subscriptions?api-version=2020-01-01"

        # Create the Invoke-RestMethod Header (using the bearertoken parameter)
        $header = @{
            "Authorization"="Bearer $BearerToken"; 
            "Content-Type" = "application/json"
        }
        #debug: $header

        # Create the Invoke-RestMethod parameters
        $invokeRestMethodParams = @{
            Uri             = $uri
            Method          = "GET"
            Headers          = $header
        }
        #debug: $invokeRestMethodParams
    #endregion

    try 
    {
        $response = $null
        # Make the REST API call with the created parameters
        $response = Invoke-RestMethod @invokeRestMethodParams
    }
    catch 
    {
        Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
    }
    # filter the response if a SubscriptionID was provided
    If (!([string]::IsNullOrEmpty($subScriptionID)))
    {
        $results = ($response.value).Where({$_.subscriptionId -like "$subScriptionID"})
    }
    else 
    {
        $results = $response.value
    }
    return $results
}

function Get-AzVM {
    <#
    .SYNOPSIS
        Retrieve the Azure VM information.
    .DESCRIPTION
        Retrieve the Azure VM information, using a REST API call.
    .EXAMPLE
        Get-AzVM -BearerToken <string> -VMname <string>
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-11-17
        Updated:        2020-11-17
                        Created a separate Azure Credentials function to support ARM architecture and REST API scripted actions

        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            HelpMessage='Enter a valid bearer token'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $BearerToken,

        [Parameter(
            Position=1, 
            Mandatory=$true, 
            HelpMessage='Enter the Subscription ID'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $SubscriptionID,
    
        [Parameter(
            Position=2, 
            Mandatory=$false, 
            HelpMessage='Enter a VM name'
        )]
        [string] $vmName
    )

    #region Prep variables
        # URL for REST API call, based on given subscription ID
        $uri = "https://management.azure.com/subscriptions/$SubscriptionID/providers/Microsoft.Compute/virtualMachines`?api-version=2020-06-01"#&`$top=5000"

        # Create the Invoke-RestMethod Header (using the bearertoken parameter)
        $header = @{
            "Authorization"="Bearer $BearerToken"; 
            "Content-Type" = "application/json"
        }
        #debug: $header

        # Create the Invoke-RestMethod parameters
        $invokeRestMethodParams = @{
            Uri             = $uri
            Method          = "GET"
            Headers          = $header
        }
        #debug: $invokeRestMethodParams
    #endregion

    try 
    {
        $response = $null
        # Make the REST API call with the created parameters
        $response = Invoke-RestMethod @invokeRestMethodParams
    }
    catch 
    {
        Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
    }
    # filter the response if a vmName was provided
    If (!([string]::IsNullOrEmpty($vmName)))
    {
        $results = ($response.value).Where({$_.name -like "$($vmName)"})
    }
    else 
    {
        $results = $response.value
    }
    return $results
}

function Get-AzCostAnalysisActualServiceByResource () {
    <#
    .SYNOPSIS
        Get Azure Cost Analysis information on the actual costs for this month sorted by Service, grouped by Resource.
    .DESCRIPTION
        Get Azure Cost Analysis information on the actual costs for this month sorted by Service, using a REST API call.
    .EXAMPLE
        Get-AzCostAnalysisActualServiceByResource -BearerToken <string> -SubscriptionID <string>
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-10-25
        Updated:        2020-10-25
                        Created a separate Azure Cost Analysis function for the costs of this month, sorted by Service

        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            HelpMessage='Enter a valid bearer token'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $BearerToken,

        [Parameter(
            Position=1, 
            Mandatory=$true, 
            HelpMessage='Enter the Subscription ID'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $SubscriptionID
    )
    #region Prep variables
        # URL for REST API call to list hostpools, based on given subscription ID
        $uri = "https://management.azure.com/subscriptions/$SubscriptionID/providers/Microsoft.CostManagement/query`?api-version=2019-11-01"#&`$top=5000"

        # Create the Invoke-RestMethod Header (using the bearertoken parameter)
        $header = @{
            "Authorization"="Bearer $BearerToken"; 
            "Content-Type" = "application/json"
        }
        #debug: $header

        # Create Custom time period for this month
        #$time = (Get-Date).AddMonths(-1).ToString("yyyy-MM")
        $addMonth = 0
        $time = $((Get-Date).AddMonths($addMonth).ToString("yyyy-MM"))
        $lastdayofmonth = "$([System.DateTime]::DaysInMonth((Get-Date).AddMonths($addMonth).Year,(Get-Date).AddMonths($addMonth).Month))"

        # Create the JSON formatted body
        $body=@{
            "type"= "ActualCost";
            "dataSet"= @{
                "granularity"= "None";
                "aggregation"= @{
                    "totalCost"= @{"name"= "Cost";"function"= "Sum"}
                };
                "grouping"= @(
                    @{"type"="Dimension";"name"="ResourceId"}; 
                    @{"type"="Dimension";"name"="ResourceType"}; 
                    @{"type"="Dimension";"name"="ResourceLocation"}; 
                    @{"type"="Dimension";"name"="ChargeType"}; 
                    @{"type"="Dimension";"name"="ResourceGroupName"}; 
                    @{"type"="Dimension";"name"="PublisherType"}; 
                    @{"type"="Dimension";"name"="ServiceName"}; 
                    @{"type"="Dimension";"name"="ServiceTier"}; 
                    @{"type"="Dimension";"name"="Meter"}
                );
                "include"=@("Tags")
            };
            "timeframe"="Custom";
            "timePeriod"=@{"from"="$time`-01T00:00:00+00:00";"to"="$time`-$lastdayofmonth`T23:59:59+00:00"}
        }
        $bodyJSON = ConvertTo-Json -InputObject $body -Depth 10

        # Create the Invoke-RestMethod parameters
        $invokeRestMethodParams = @{
            Uri             = $uri
            Method          = "POST"
            Headers         = $header
            Body            = $bodyJSON
        }
        #debug: $invokeRestMethodParams
    #endregion

    try 
    {
        $response = $null
        # Make the REST API call with the created parameters
        $response = Invoke-RestMethod @invokeRestMethodParams
    }
    catch 
    {
        Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
    }
    #debug: $response
    $results = $response.properties
    return $results
}

function Get-AzCostAnalysisForecastServiceByResource () {
    <#
    .SYNOPSIS
        Get Azure Cost Analysis information on the forecasted costs for this month sorted by Service, grouped by Resource.
    .DESCRIPTION
        Get Azure Cost Analysis information on the forecasted costs for this month sorted by Service, using a REST API call.
    .EXAMPLE
        Get-AzCostAnalysisForecastServiceByResource -BearerToken <string> -SubscriptionID <string>
    .CONTEXT
        Azure
    .NOTES
        Version:        0.1
        Author:         Esther Barthel, MSc
        Creation Date:  2020-10-25
        Updated:        2020-10-25
                        Created a separate Azure Cost Analysis function to support ARM architecture and REST API scripted actions

        Purpose:        WVD Administration, through REST API calls
        
        Copyright (c) cognition IT. All rights reserved.
    #>
    [CmdletBinding()]
    Param(
        [Parameter(
            Position=0, 
            Mandatory=$true, 
            HelpMessage='Enter a valid bearer token'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $BearerToken,

        [Parameter(
            Position=1, 
            Mandatory=$true, 
            HelpMessage='Enter the Subscription ID'
        )]
        [ValidateNotNullOrEmpty()]
        [string] $SubscriptionID
    )
    #region Prep variables
        # URL for REST API call to list hostpools, based on given subscription ID
        $uri = "https://management.azure.com/subscriptions/$SubscriptionID/providers/Microsoft.CostManagement/forecast`?api-version=2019-11-01"#&`$top=5000"

        # Create the Invoke-RestMethod Header (using the bearertoken parameter)
        $header = @{
            "Authorization"="Bearer $BearerToken"; 
            "Content-Type" = "application/json"
        }
        #debug: $header

        # Create Custom time period for this month
        #$time = (Get-Date).AddMonths(-1).ToString("yyyy-MM")
        $addMonth=0
        $time = $((Get-Date).AddMonths($addMonth).ToString("yyyy-MM"))
        $lastdayofmonth = "$([System.DateTime]::DaysInMonth((Get-Date).AddMonths($addMonth).Year,(Get-Date).AddMonths($addMonth).Month))"

        # Create the JSON formatted body
        $body=@{
            "type"= "ActualCost";
            "dataSet"= @{
                "granularity"= "None";
                "aggregation"= @{
                    "totalCost"= @{"name"= "Cost";"function"= "Sum"}
                };
                "grouping"= @(
                    @{"type"="Dimension";"name"="ResourceId"}; 
                    @{"type"="Dimension";"name"="ResourceType"}; 
                    @{"type"="Dimension";"name"="ResourceLocation"}; 
                    @{"type"="Dimension";"name"="ChargeType"}; 
                    @{"type"="Dimension";"name"="ResourceGroupName"}; 
                    @{"type"="Dimension";"name"="PublisherType"}; 
                    @{"type"="Dimension";"name"="ServiceName"}; 
                    @{"type"="Dimension";"name"="ServiceTier"}; 
                    @{"type"="Dimension";"name"="Meter"}
                );
                "include"=@("Tags")
            };
            "timeframe"="Custom";
            "timePeriod"=@{"from"="$time`-01T00:00:00+00:00";"to"="$time`-$lastdayofmonth`T23:59:59+00:00"};
            "includeActualCost"="false";
            "includeFreshPartialCost"="false"
        }
        $bodyJSON = ConvertTo-Json -InputObject $body -Depth 10

        # Create the Invoke-RestMethod parameters
        $invokeRestMethodParams = @{
            Uri             = $uri
            Method          = "POST"
            Headers         = $header
            Body            = $bodyJSON
        }
        #debug: $invokeRestMethodParams
    #endregion

    try 
    {
        $response = $null
        # Make the REST API call with the created parameters
        $response = Invoke-RestMethod @invokeRestMethodParams
    }
    catch 
    {
        Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
    }
    #debug: $response
    $results = $response.properties
    return $results
}

#region ControlUp Script Standards - version 0.2
    #Requires -Version 5.1

    # Configure a larger output width for the ControlUp PowerShell console
    [int]$outputWidth = 400
    # Altering the size of the PS Buffer
    $PSWindow = (Get-Host).UI.RawUI
    $WideDimensions = $PSWindow.BufferSize
    $WideDimensions.Width = $outputWidth
    $PSWindow.BufferSize = $WideDimensions

    # Ensure Debug information is shown, without the confirmation question after each Write-Debug
    If ($PSBoundParameters['Debug']) {$DebugPreference = "Continue"}
    If ($PSBoundParameters['Verbose']) {$VerbosePreference = "Continue"}
    $ErrorActionPreference = "Stop"
#endregion



#-----------------#
# Script Workflow #
#-----------------#
Write-Output ""

# Script output
If ($azSPCredentials = Get-AzSPStoredCredentials)
{
    #debug: $azSPCredentials
    # Sign in to Azure with a Service Principal with Contributor Role at Subscription level and retrieve the brearer token
    try
    {
        $azBearerToken = $null
        $azBearerToken = Get-AzBearerToken -SPCredentials $azSPCredentials.spCreds -TenantID $($azSPCredentials.tenantID).ToString()
        #debug: $azBearerToken
    }
    catch
    {
        Write-Error ("A [" + $_.Exception.GetType().FullName + "] ERROR occurred. " + $_.Exception.Message)
        Exit
    }

    # Retrieve the Subscription information for the Service Principal (that is logged on)
    $azSubscription = $null
    $azSubscription = Get-AzSubscription -bearerToken $($azBearerToken.access_token)
    #debug: Write-Output "DEBUG INFO - subscriptionID: $($azSubscription.subscriptionId)"


    # Retrieve the VM information
    $azVM = $null
    $azVM = (Get-AzVM -bearerToken $($azBearerToken.access_token) -SubscriptionID $($azSubscription.subscriptionId.Split("/")[-1]) -vmName $vmName)
    #debug: Write-Output "DEBUG INFO - VM: $($azVM)"

    If ($(($azVM | Measure-Object).Count) -gt 1)
    {
        Write-Warning "More than one VM found, selecting first VM in list as resource."
        Write-Output ""
    }
    If ($(($azVM | Measure-Object).Count) -ge 1)
    {
        # Only process the first entry as we need do not want to blow up the Where ScriptBlock
        $selectedVMName = $($azVM[0].name)
        # Creating a dynamic Where filter
        $whereArray = @()
        # Add vmName to Where ScriptBlock
        $whereArray += "(`$_.Resource -like ""$($azVM[0].name)"")"
        # Add osDisk to Where ScriptBlock
        $whereArray += "(`$_.Resource -like ""$($azVM[0].properties.storageProfile.osDisk.name)"")"
        # Add dataDisks to Where ScriptBlock
        foreach ($dataDisk in $($azVM[0].properties.storageProfile.dataDisks))
        {
            $whereArray += "(`$_.Resource -like ""$($dataDisk.name)"")"
        }
        #Build the where array into a string by joining each statement with -and            
        $whereString = $whereArray -Join " -or "
        #debug: Write-Output "DEBUG INFO - whereString: $($whereString)"
            
        #Create the scriptblock with your final string            
        $whereBlock = [scriptblock]::Create($whereString)
    }

    # Retrieve the Cost Analysis - Actual Costs details
    $costAnalysisResults = Get-AzCostAnalysisActualServiceByResource -bearerToken $($azBearerToken.access_token) -SubscriptionID $($azSubscription.subscriptionId.Split("/")[-1])

    # Build output dataset, based on custom object
    $dataResults = @()
    for ($row=0;$row -le (($costAnalysisResults.rows.Count)-1); $row++)
    {
        $htrow = New-Object psobject
        for ($col=0;$col -le (($costAnalysisResults.columns.Count)-1); $col++)
        {
            $htrow | Add-Member -Type NoteProperty -Name "$($costAnalysisResults.columns[$col].name)" -Value "$($costAnalysisResults.rows[$row][$col])"
        }
        $dataResults += $htrow
    }
    # Select the Cost information
    $dataset = $dataResults | Select Cost, 
        ResourceId, 
        @{Name='ResourceType'; Expression={$($_.ResourceType.Split("/")[-1])}}, 
        ResourceLocation, 
        ChargeType, 
        ResourceGroupName, 
        PublisherType, 
        ServiceName, 
        ServiceTier, 
        Meter, 
        Tags, 
        @{Name='Resource'; Expression={$($_.ResourceId.Split("/")[-1])}}, 
        @{Name='Costs'; Expression={$([math]::Round($_.Cost,2))}}, 
        @{Name='Location'; Expression={$_.ResourceLocation}}, 
        @{Name='Resource Group'; Expression={$($_.ResourceGroupName)}}, 
        @{Name='Percentage'; Expression={$([math]::Round((($_.Cost/$totalCosts)*100),0))}}, 
        Currency | 
            Sort Costs -Descending | 
            Sort Resource 

    # Filter the Cost Analysis - Actual Costs by Resource for VM and corresponding disks
    $vmCostsDataset = $dataset | Where -FilterScript $whereBlock

    # Retrieve the Cost Analysis - Forecast details
    $forecastAnalysisResults = Get-AzCostAnalysisForecastServiceByResource -bearerToken $($azBearerToken.access_token) -SubscriptionID $($azSubscription.subscriptionId.Split("/")[-1]) #-ResourceGroupName $ResourceGroupName

    # Build output dataset, based on custom object
    $forecastResults = @()
    for ($row=0;$row -le (($forecastAnalysisResults.rows.Count)-1); $row++)
    {
        $fhtrow = New-Object psobject
        for ($col=0;$col -le (($forecastAnalysisResults.columns.Count)-1); $col++)
        {
            $fhtrow | Add-Member -Type NoteProperty -Name "$($forecastAnalysisResults.columns[$col].name)" -Value "$($forecastAnalysisResults.rows[$row][$col])"
        }
        $forecastResults += $fhtrow
    }

    # Calculate the Summary information
    $totalVMCosts = $([math]::Round((($vmCostsDataset | Measure-Object -Property Cost -Sum).Sum),2))
    $totalActualCosts = $([math]::Round((($dataResults | Measure-Object -Property Cost -Sum).Sum),2))
    $totalForecastCosts = $([math]::Round( (($forecastResults | Measure-Object -Property Cost -Sum).Sum),2))

    # Present the results
    Write-Host "Actual costs breakdown by service associated with VM: " -ForegroundColor Yellow -NoNewline
    Write-Host "$($selectedVMName)" -ForegroundColor Cyan
    $vmCostsDataset | 
        Where {$_.Costs -gt 0} | 
            Select ResourceId, ServiceName, ServiceTier, Meter, Costs, Currency | 
                Sort Resource, ServiceName, ServiceTier, Meter | 
                    Format-Table @{Name='Resource';Expression={$_.ResourceId.Split("/")[-1]}},
                        @{Name='Service name';Expression={$_.ServiceName}}, 
                        @{Name='Service tier';Expression={$_.ServiceTier}}, 
                        Meter, 
                        @{Name='Costs         '; Expression={"$($_.Currency) {0,10:N2}" -f($($_.Costs))}; Align="right"}
    Write-Host "Total costs for this VM: " -ForegroundColor Yellow -NoNewline
    Write-Host "$($vmCostsDataset[0].Currency) $($totalVMCosts)" -ForegroundColor Cyan -NoNewline
    Write-Host ", which is " -ForegroundColor Yellow -NoNewline
    Write-Host "~$([math]::Round((($totalVMCosts/$totalActualCosts)*100),2)) % " -ForegroundColor Cyan -NoNewline
    Write-Host "of the actual Azure subscription costs for this month " -ForegroundColor Yellow -NoNewline
    Write-Host "($($dataResults[0].Currency) $($totalActualCosts))" -ForegroundColor Cyan
}
else 
{
    Write-Warning "No Azure Credentials could be retrieved from the stored credentials file for this user."
}