From a18f5168d414e0cbd43cc46c2b20da4075d281c7 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:22:19 +0200 Subject: [PATCH 1/7] initial draft --- 1-Collect/Get-AzureServices.ps1 | 100 ++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index 0703ffa..5782278 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -41,16 +41,26 @@ .FUNCTION Get-Method Determines the appropriate method to retrieve resource-specific data based on the resource type and flag type. +.FUNCTION New-CostReport + Generates a cost report for a specified subscription by invoking the Azure REST API and retrieves cost details + for the previous month. + +.FUNCTION Get-CostReportDetails + Fetches the cost report details from the Azure REST API and processes the results. + +.FUNCTION Get-MeterIds + Retrieves unique meter IDs associated with a specific resource ID from the cost details CSV. + .EXAMPLE - PS C:\> .\assess_resources.ps1 -scopeType singleSubscription -subscriptionId "12345678-1234-1234-1234-123456789abc" + PS C:\> .\Get-AzureServices.ps1 -scopeType singleSubscription -subscriptionId "12345678-1234-1234-1234-123456789abc" Runs the script for a single subscription with the specified subscription ID and outputs the results to the default file. .EXAMPLE - PS C:\> .\assess_resources.ps1 -scopeType resourceGroup -resourceGroupName "MyResourceGroup" + PS C:\> .\Get-AzureServices.ps1 -scopeType resourceGroup -resourceGroupName "MyResourceGroup" Runs the script for a specific resource group within the current subscription and outputs the results to the default file. .EXAMPLE - PS C:\> .\assess_resources.ps1 -scopeType multiSubscription -workloadFile "subscriptions.json" -fullOutputFile "output.json" + PS C:\> .\Get-AzureServices.ps1 -scopeType multiSubscription -workloadFile "subscriptions.json" -fullOutputFile "output.json" Runs the script for multiple subscriptions defined in the workload file and outputs the results to "output.json". @@ -97,9 +107,13 @@ Function Get-MultiLoop { foreach ($subscription in $workloads.subscriptions) { $basequery = "resources | where subscriptionId == '$subscription'" Get-SingleData -query $basequery + New-CostReport -SubscriptionId $subscription + Get-CostReportDetails -PathForResult $pathForResult + $tempCostArray += $Script:costdetails $tempArray += $Script:baseresult } $Script:baseresult = $tempArray + $Script:costdetails = $tempCostArray } Function Get-Property { @@ -191,6 +205,68 @@ Function Get-Method { } } +Function New-CostReport { + param ( + [Parameter(Mandatory = $true)] [string]$SubscriptionId + ) + $uri = "https://management.azure.com/subscriptions/$($SubscriptionId)/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2025-03-01" + # Get Previous month + $lastMonth = (Get-Date).AddMonths(-1) + $startDate = Get-Date -Year $lastMonth.Year -Month $lastMonth.Month -Day 1 + $endDate = $startDate.AddMonths(1).AddDays(-1) + # Define the request body + $body = @{ + metric = "ActualCost" + timePeriod = @{ + start = $startDate.ToString("yyyy-MM-dd") + end = $endDate.ToString("yyyy-MM-dd") + } + } + #Convert the body to JSON + $bodyJson = $body | ConvertTo-Json + $result = invoke-AzRestMethod -Uri $uri -Method POST -Payload $bodyJson + $pathForResult = "https://management.azure.com" + $result.Headers.Location.AbsolutePath + "?api-version=2025-03-01" + write-output "Cost report request submitted. Path for result: $pathForResult" + Set-Variable -Name 'pathForResult' -Value $pathForResult -Scope Script +} + +Function Get-CostReportDetails { + param ( + [Parameter(Mandatory = $true)] [string]$PathForResult + ) + $i = 0 + $details = Invoke-AzRestMethod -uri $PathForResult -Method GET + # Loop until $details.statuscode is 200 + while ($details.StatusCode -eq 202) { + Start-Sleep -Seconds 10 + $i = $i + 10 + $details = Invoke-AzRestMethod -uri $PathForResult -Method GET + Write-Output "Waiting for the cost report to be ready. Elapsed time: $i seconds" + } + "Cost report is ready. Downloading the report..." + $subscriptionID = (($details.Content | ConvertFrom-Json).manifest.requestContext.requestScope) -replace "^/subscriptions/", "" -replace "/$", "" + $blobLink = ($details.Content | ConvertFrom-Json).manifest.blobs.bloblink + $blobContent = Invoke-RestMethod -Uri $blobLink -Method Get + $blobContent | out-file "$subscriptionID.csv" + $csv = Import-Csv -Path "$subscriptionID.csv" + Set-Variable -name costdetails -Value $csv -Scope Script +} + +Function Get-MeterIds { + param ( + [Parameter(Mandatory = $true)] [string]$ResourceId, + [Parameter(Mandatory = $true)] [PSCustomObject]$csvObject + ) + $outputArray = @() + $meterIds = $csvObject | Where-Object { $_.resourceId -eq $ResourceId } | Select-Object meterId -Unique + # For each meterId, get the meterId value and add it to the output array + foreach ($meterId in $meterIds) { + $outputArray += $meterId.meterId + } + Set-Variable -Name 'meterIds' -Value $outputArray -Scope Script +} + + # Main script starts here # Turn off breaking change warnings for Azure PowerShell, for Get-AzMetric CmdLet Set-Item -Path Env:\SuppressAzurePowerShellBreakingChangeWarnings -Value $true @@ -204,6 +280,8 @@ Switch ($scopeType) { } $baseQuery = "resources | where subscriptionId == '$subscriptionId'" Get-SingleData -query $baseQuery + New-CostReport -SubscriptionId $subscriptionId + Get-CostReportDetails -PathForResult $pathForResult } 'resourceGroup' { # KQL Query to get all resources in a specific resource group and subscription @@ -212,6 +290,8 @@ Switch ($scopeType) { } $baseQuery = "resources | where resourceGroup == '$resourceGroupName' and subscriptionId == '$subscriptionId'" Get-SingleData -query $baseQuery + New-CostReport -SubscriptionId $subscriptionId + Get-CostReportDetails -PathForResult $pathForResult } 'multiSubscription' { "multiple subscriptions" @@ -237,6 +317,10 @@ $baseResult | ForEach-Object { Get-Method -resourceType $resourceType -flagType "resiliencyProperties" -object $PSItem Get-Method -resourceType $resourceType -flagType "dataSize" -object $PSItem Get-Method -resourceType $resourceType -flagType "ipConfig" -object $PSItem + # Get csvFile with cost details + #$costDetails = Import-Csv -Path "$($subscriptionId).csv" + $resourceSubscriptionId + Get-MeterIds -ResourceId $resourceId -csvObject $costDetails $outObject = [PSCustomObject] @{ ResourceType = $resourceType ResourceName = $resourceName @@ -248,14 +332,20 @@ $baseResult | ForEach-Object { resiliencyProperties = $resiliencyProperties dataSizeGB = $dataSize ipAddress = $ipAddress + meterIds = $meterIds } $outputArray += $outObject } $outputArray | ConvertTo-Json -Depth 100 | Out-File -FilePath $fullOutputFile +$global:myArray = $outputArray $groupedResources = $outputArray | Group-Object -Property ResourceType $summary = @() foreach ($group in $groupedResources) { $resourceType = $group.Name + $uniqueMeterIds = $group.Group | Select-Object -Property meterIds -Unique | Select-Object -ExpandProperty meterIds + if ($uniqueMeterIds -isnot [System.Array]) { + $uniqueMeterIds = @($uniqueMeterIds) + } $uniqueLocations = $group.Group | Select-Object -Property ResourceLocation -Unique | Select-Object -ExpandProperty ResourceLocation if ($uniqueLocations -isnot [System.Array]) { $uniqueLocations = @($uniqueLocations) @@ -263,10 +353,10 @@ foreach ($group in $groupedResources) { If ($group.Group.ResourceSku -ne 'N/A') { $uniqueSkus = $group.Group.ResourceSku | Select-Object * -Unique - $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = $uniqueSkus; AzureRegions = $uniqueLocations } + $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = $uniqueSkus; AzureRegions = $uniqueLocations; meterIds = $uniqueMeterIds } } Else { - $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = @("N/A"); AzureRegions = $uniqueLocations } + $summary += [PSCustomObject]@{ResourceCount = $group.Count; ResourceType = $resourceType; ResourceSkus = @("N/A"); AzureRegions = $uniqueLocations; meterIds = $uniqueMeterIds } } } $summary | ConvertTo-Json -Depth 100 | Out-File -FilePath $summaryOutputFile \ No newline at end of file From d279a505f1f39a74e2eda76a4065d4f596398fa0 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:58:57 +0200 Subject: [PATCH 2/7] Tested --- 1-Collect/Get-AzureServices.ps1 | 48 ++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index 5782278..d1c6476 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -25,6 +25,10 @@ .PARAMETER summaryOutputFile The name of the output file where the summary will be exported. Default is "summary.json". +.PARAMETER includeCost + A boolean flag indicating whether to include cost report generation. Default is $false. Note that this requires the identity + running the script to have permissions to access cost management APIs, i.e. Cost Management Contributor role. + .FUNCTION Get-SingleData Queries Azure Resource Graph for resources within a single subscription and retrieves all results, handling pagination if necessary. @@ -79,7 +83,8 @@ param( [Parameter(Mandatory = $false)] [string] $resourceGroupName, # resource group to run the query against [Parameter(Mandatory = $false)] [string] $workloadFile, # JSON file containing subscriptions [Parameter(Mandatory = $false)] [string] $fullOutputFile = "resources.json", # Json file to export the results to - [Parameter(Mandatory = $false)] [string] $summaryOutputFile = "summary.json" # Json file to export the results to + [Parameter(Mandatory = $false)] [string] $summaryOutputFile = "summary.json", # Json file to export the results to + [Parameter(Mandatory = $false)] [bool] $includeCost = $false # Include cost report ) Function Get-SingleData { @@ -107,10 +112,12 @@ Function Get-MultiLoop { foreach ($subscription in $workloads.subscriptions) { $basequery = "resources | where subscriptionId == '$subscription'" Get-SingleData -query $basequery - New-CostReport -SubscriptionId $subscription - Get-CostReportDetails -PathForResult $pathForResult - $tempCostArray += $Script:costdetails $tempArray += $Script:baseresult + If ($includeCost) { + New-CostReport -SubscriptionId $subscription + Get-CostReportDetails -PathForResult $pathForResult + $tempCostArray += $Script:costdetails + } } $Script:baseresult = $tempArray $Script:costdetails = $tempCostArray @@ -210,10 +217,8 @@ Function New-CostReport { [Parameter(Mandatory = $true)] [string]$SubscriptionId ) $uri = "https://management.azure.com/subscriptions/$($SubscriptionId)/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2025-03-01" - # Get Previous month - $lastMonth = (Get-Date).AddMonths(-1) - $startDate = Get-Date -Year $lastMonth.Year -Month $lastMonth.Month -Day 1 - $endDate = $startDate.AddMonths(1).AddDays(-1) + $startDate =(Get-Date).AddDays(-1) + $endDate = (Get-Date) # Define the request body $body = @{ metric = "ActualCost" @@ -280,8 +285,11 @@ Switch ($scopeType) { } $baseQuery = "resources | where subscriptionId == '$subscriptionId'" Get-SingleData -query $baseQuery - New-CostReport -SubscriptionId $subscriptionId - Get-CostReportDetails -PathForResult $pathForResult + If ($includeCost) { + # Generate cost report for the subscription + New-CostReport -SubscriptionId $subscriptionId + Get-CostReportDetails -PathForResult $pathForResult + } } 'resourceGroup' { # KQL Query to get all resources in a specific resource group and subscription @@ -290,8 +298,11 @@ Switch ($scopeType) { } $baseQuery = "resources | where resourceGroup == '$resourceGroupName' and subscriptionId == '$subscriptionId'" Get-SingleData -query $baseQuery - New-CostReport -SubscriptionId $subscriptionId - Get-CostReportDetails -PathForResult $pathForResult + If ($includeCost) { + # Generate cost report for the subscription + New-CostReport -SubscriptionId $subscriptionId + Get-CostReportDetails -PathForResult $pathForResult + } } 'multiSubscription' { "multiple subscriptions" @@ -317,10 +328,6 @@ $baseResult | ForEach-Object { Get-Method -resourceType $resourceType -flagType "resiliencyProperties" -object $PSItem Get-Method -resourceType $resourceType -flagType "dataSize" -object $PSItem Get-Method -resourceType $resourceType -flagType "ipConfig" -object $PSItem - # Get csvFile with cost details - #$costDetails = Import-Csv -Path "$($subscriptionId).csv" - $resourceSubscriptionId - Get-MeterIds -ResourceId $resourceId -csvObject $costDetails $outObject = [PSCustomObject] @{ ResourceType = $resourceType ResourceName = $resourceName @@ -334,10 +341,14 @@ $baseResult | ForEach-Object { ipAddress = $ipAddress meterIds = $meterIds } + If ($includeCost) { + Get-MeterIds -ResourceId $resourceId -csvObject $costDetails + # add meterIds to the output object + $outObject.meterIds += $meterIds + } $outputArray += $outObject } $outputArray | ConvertTo-Json -Depth 100 | Out-File -FilePath $fullOutputFile -$global:myArray = $outputArray $groupedResources = $outputArray | Group-Object -Property ResourceType $summary = @() foreach ($group in $groupedResources) { @@ -345,7 +356,8 @@ foreach ($group in $groupedResources) { $uniqueMeterIds = $group.Group | Select-Object -Property meterIds -Unique | Select-Object -ExpandProperty meterIds if ($uniqueMeterIds -isnot [System.Array]) { $uniqueMeterIds = @($uniqueMeterIds) - } + } + $uniqueMeterIds = $uniqueMeterIds | Select-Object -Unique $uniqueLocations = $group.Group | Select-Object -Property ResourceLocation -Unique | Select-Object -ExpandProperty ResourceLocation if ($uniqueLocations -isnot [System.Array]) { $uniqueLocations = @($uniqueLocations) From ee9d363607f9e0d89e1273fa07f8f9de608f70d8 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:19:36 +0200 Subject: [PATCH 3/7] singlenoun --- 1-Collect/Get-AzureServices.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index d1c6476..f2fcd30 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -49,10 +49,10 @@ Generates a cost report for a specified subscription by invoking the Azure REST API and retrieves cost details for the previous month. -.FUNCTION Get-CostReportDetails +.FUNCTION Get-CostReport Fetches the cost report details from the Azure REST API and processes the results. -.FUNCTION Get-MeterIds +.FUNCTION Get-MeterId Retrieves unique meter IDs associated with a specific resource ID from the cost details CSV. .EXAMPLE @@ -115,7 +115,7 @@ Function Get-MultiLoop { $tempArray += $Script:baseresult If ($includeCost) { New-CostReport -SubscriptionId $subscription - Get-CostReportDetails -PathForResult $pathForResult + Get-CostReport -PathForResult $pathForResult $tempCostArray += $Script:costdetails } } @@ -235,7 +235,7 @@ Function New-CostReport { Set-Variable -Name 'pathForResult' -Value $pathForResult -Scope Script } -Function Get-CostReportDetails { +Function Get-CostReport { param ( [Parameter(Mandatory = $true)] [string]$PathForResult ) @@ -257,7 +257,7 @@ Function Get-CostReportDetails { Set-Variable -name costdetails -Value $csv -Scope Script } -Function Get-MeterIds { +Function Get-MeterId { param ( [Parameter(Mandatory = $true)] [string]$ResourceId, [Parameter(Mandatory = $true)] [PSCustomObject]$csvObject @@ -288,7 +288,7 @@ Switch ($scopeType) { If ($includeCost) { # Generate cost report for the subscription New-CostReport -SubscriptionId $subscriptionId - Get-CostReportDetails -PathForResult $pathForResult + Get-CostReport -PathForResult $pathForResult } } 'resourceGroup' { @@ -301,7 +301,7 @@ Switch ($scopeType) { If ($includeCost) { # Generate cost report for the subscription New-CostReport -SubscriptionId $subscriptionId - Get-CostReportDetails -PathForResult $pathForResult + Get-CostReport -PathForResult $pathForResult } } 'multiSubscription' { @@ -342,7 +342,7 @@ $baseResult | ForEach-Object { meterIds = $meterIds } If ($includeCost) { - Get-MeterIds -ResourceId $resourceId -csvObject $costDetails + Get-MeterId -ResourceId $resourceId -csvObject $costDetails # add meterIds to the output object $outObject.meterIds += $meterIds } From 4f90fc7dfd823e433cc8a91c92eb60d5ebf04260 Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:39:08 +0200 Subject: [PATCH 4/7] shouldprocess --- 1-Collect/Get-AzureServices.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index f2fcd30..0761d48 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -45,7 +45,7 @@ .FUNCTION Get-Method Determines the appropriate method to retrieve resource-specific data based on the resource type and flag type. -.FUNCTION New-CostReport +.FUNCTION Invoke-CostReportSchedule Generates a cost report for a specified subscription by invoking the Azure REST API and retrieves cost details for the previous month. @@ -114,7 +114,7 @@ Function Get-MultiLoop { Get-SingleData -query $basequery $tempArray += $Script:baseresult If ($includeCost) { - New-CostReport -SubscriptionId $subscription + Invoke-CostReportSchedule -SubscriptionId $subscription Get-CostReport -PathForResult $pathForResult $tempCostArray += $Script:costdetails } @@ -212,7 +212,7 @@ Function Get-Method { } } -Function New-CostReport { +Function Invoke-CostReportSchedule { param ( [Parameter(Mandatory = $true)] [string]$SubscriptionId ) @@ -287,7 +287,7 @@ Switch ($scopeType) { Get-SingleData -query $baseQuery If ($includeCost) { # Generate cost report for the subscription - New-CostReport -SubscriptionId $subscriptionId + Invoke-CostReportSchedule -SubscriptionId $subscriptionId Get-CostReport -PathForResult $pathForResult } } @@ -300,7 +300,7 @@ Switch ($scopeType) { Get-SingleData -query $baseQuery If ($includeCost) { # Generate cost report for the subscription - New-CostReport -SubscriptionId $subscriptionId + Invoke-CostReportSchedule -SubscriptionId $subscriptionId Get-CostReport -PathForResult $pathForResult } } From 5b1606f4e92727c3cf454ccca811cff897da581c Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:35:28 +0200 Subject: [PATCH 5/7] fix duplicate meterid --- 1-Collect/Get-AzureServices.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index 0761d48..6ccc781 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -263,9 +263,11 @@ Function Get-MeterId { [Parameter(Mandatory = $true)] [PSCustomObject]$csvObject ) $outputArray = @() - $meterIds = $csvObject | Where-Object { $_.resourceId -eq $ResourceId } | Select-Object meterId -Unique + #Reset variable to avoid conflicts + Set-Variable -Name 'meterIds' -Value @() -scope script + $resMeterIds = $csvObject | Where-Object { $_.resourceId -eq $ResourceId } | Select-Object meterId -Unique # For each meterId, get the meterId value and add it to the output array - foreach ($meterId in $meterIds) { + foreach ($meterId in $resMeterIds) { $outputArray += $meterId.meterId } Set-Variable -Name 'meterIds' -Value $outputArray -Scope Script @@ -339,7 +341,7 @@ $baseResult | ForEach-Object { resiliencyProperties = $resiliencyProperties dataSizeGB = $dataSize ipAddress = $ipAddress - meterIds = $meterIds + meterIds = @() } If ($includeCost) { Get-MeterId -ResourceId $resourceId -csvObject $costDetails From ca49854d24099b3957dc02f4720bbd6aeadc15ba Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Tue, 8 Jul 2025 15:34:22 +0200 Subject: [PATCH 6/7] better handling of strings and null values --- 1-Collect/Get-AzureServices.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index 6ccc781..9e48d3e 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -356,10 +356,10 @@ $summary = @() foreach ($group in $groupedResources) { $resourceType = $group.Name $uniqueMeterIds = $group.Group | Select-Object -Property meterIds -Unique | Select-Object -ExpandProperty meterIds + $uniqueMeterIds = $uniqueMeterIds | Select-Object -Unique if ($uniqueMeterIds -isnot [System.Array]) { $uniqueMeterIds = @($uniqueMeterIds) } - $uniqueMeterIds = $uniqueMeterIds | Select-Object -Unique $uniqueLocations = $group.Group | Select-Object -Property ResourceLocation -Unique | Select-Object -ExpandProperty ResourceLocation if ($uniqueLocations -isnot [System.Array]) { $uniqueLocations = @($uniqueLocations) From 54a0d83b53dda879aae3ebf12515344d80ab429a Mon Sep 17 00:00:00 2001 From: Jan Faurskov <22591930+jfaurskov@users.noreply.github.com> Date: Wed, 9 Jul 2025 08:31:15 +0200 Subject: [PATCH 7/7] amortizedcost --- 1-Collect/Get-AzureServices.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/1-Collect/Get-AzureServices.ps1 b/1-Collect/Get-AzureServices.ps1 index 9e48d3e..e025059 100644 --- a/1-Collect/Get-AzureServices.ps1 +++ b/1-Collect/Get-AzureServices.ps1 @@ -221,7 +221,7 @@ Function Invoke-CostReportSchedule { $endDate = (Get-Date) # Define the request body $body = @{ - metric = "ActualCost" + metric = "AmortizedCost" timePeriod = @{ start = $startDate.ToString("yyyy-MM-dd") end = $endDate.ToString("yyyy-MM-dd")