All scripts
Azure AI 662

Azure Orphaned Managed Disk Report

Finds managed disks in Azure that are not attached to any VM, so you can review and clean up disks that are quietly costing money every month.

Get-AzureOrphanedDiskReport.ps1
<#
.SYNOPSIS
    Reports on Azure managed disks that are not attached to any virtual machine.

.DESCRIPTION
    Unattached managed disks keep billing every month even though nothing is using them.
    This script scans the current subscription, finds every managed disk with a disk state
    of "Unattached", and reports the disk name, resource group, size, SKU, region, and how
    many days it has existed. Use it to catch leftover disks from deleted VMs, failed
    deployments, or old test environments before they add up on the bill.

.AUTHOR
    Shehryar Hassan

.EXAMPLE
    .\Get-AzureOrphanedDiskReport.ps1

    Scans the current subscription context and prints a table of orphaned disks with their
    size, SKU, and age.

.EXAMPLE
    .\Get-AzureOrphanedDiskReport.ps1 -SubscriptionId "11111111-2222-3333-4444-555555555555" -ExportPath "C:\Reports\orphaned-disks.csv"

    Scans a specific subscription and exports the results to CSV.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [string]$SubscriptionId,

    [Parameter(Mandatory = $false)]
    [string]$ExportPath
)

if (-not (Get-Module -ListAvailable -Name Az.Compute)) {
    Write-Error "The Az.Compute module is not installed. Run: Install-Module -Name Az -Scope CurrentUser"
    return
}

if ($SubscriptionId) {
    Write-Host "Setting context to subscription $SubscriptionId" -ForegroundColor Cyan
    Set-AzContext -SubscriptionId $SubscriptionId | Out-Null
}

$context = Get-AzContext
if (-not $context) {
    Write-Error "No active Azure context found. Run Connect-AzAccount first."
    return
}

Write-Host "Scanning subscription: $($context.Subscription.Name)" -ForegroundColor Cyan

$allDisks = Get-AzDisk
$orphanedDisks = $allDisks | Where-Object { $_.DiskState -eq "Unattached" }

if (-not $orphanedDisks -or $orphanedDisks.Count -eq 0) {
    Write-Host "No unattached managed disks found. Nothing to clean up." -ForegroundColor Green
    return
}

$report = foreach ($disk in $orphanedDisks) {
    $daysSinceCreated = if ($disk.TimeCreated) {
        [math]::Round(((Get-Date) - $disk.TimeCreated).TotalDays)
    }
    else {
        $null
    }

    [PSCustomObject]@{
        DiskName         = $disk.Name
        ResourceGroup    = $disk.ResourceGroupName
        Region           = $disk.Location
        SizeGB           = $disk.DiskSizeGB
        Sku              = $disk.Sku.Name
        DaysSinceCreated = $daysSinceCreated
    }
}

$report = $report | Sort-Object -Property SizeGB -Descending

Write-Host ""
Write-Host "Found $($report.Count) unattached managed disk(s):" -ForegroundColor Yellow
$report | Format-Table -AutoSize

$totalGB = ($report | Measure-Object -Property SizeGB -Sum).Sum
Write-Host ""
Write-Host "Total unattached capacity: $totalGB GB across $($report.Count) disk(s)."
Write-Host "Review each disk before deleting, some are kept intentionally as backups or snapshots pending restore."

if ($ExportPath) {
    $report | Export-Csv -Path $ExportPath -NoTypeInformation
    Write-Host "Report exported to $ExportPath" -ForegroundColor Green
}

Read it before you run it, and test in a safe tenant first.