Powershell Script to go through Partner Center Customers and find Specific Server OS

<#
.SYNOPSIS
    Finds all Windows Server 2016 VMs across every customer Azure subscription
    reachable via your active GDAP relationships.

.DESCRIPTION
    Reads the customer tenant list produced by Get-GDAPCustomerTenants.ps1
    (run separately, in its own process, to avoid a Microsoft.Graph/Az.Accounts
    assembly conflict), signs into Azure PowerShell, and for each customer
    tenant:
      - Lists Azure subscriptions. Tenants where your GDAP role doesn't map to
        Azure RBAC (or the mapping hasn't been granted) fail here - logged to
        the errors CSV and skipped, not a script bug.
      - Runs an Azure Resource Graph query across all VMs, flagging any whose
        OS image indicates Windows Server 2016.
      - Falls back to a per-VM instance-view check (Get-AzVM -Status) for
        Windows VMs whose image reference is ambiguous (custom/generalized
        image, or in-place upgrade).

    Exports two CSVs: confirmed Server 2016 findings, and a log of
    tenants/subscriptions that couldn't be checked (and why).

.PARAMETER CustomerCsv
    Path to the customer tenant list from Get-GDAPCustomerTenants.ps1.

.PARAMETER OutputPath
    Folder to write the CSV reports to. Defaults to the script's own folder.
#>

[CmdletBinding()]
param(
    [string]$CustomerCsv = (Join-Path $PSScriptRoot 'GDAPCustomers.csv'),
    [string]$OutputPath  = $PSScriptRoot
)

$ErrorActionPreference = 'Stop'

if (-not (Test-Path $CustomerCsv)) {
    throw "Customer list not found at '$CustomerCsv'. Run Get-GDAPCustomerTenants.ps1 first."
}

# Multiple Az module versions are installed side-by-side on this machine.
# Pin the newest of each explicitly, Az.Accounts first, so a stale auto-picked
# version doesn't get loaded ahead of what the others require.
$moduleVersions = [ordered]@{
    'Az.Accounts'      = '5.5.3'
    'Az.Resources'     = '7.1.0'
    'Az.ResourceGraph' = '1.0.0'
    'Az.Compute'       = '11.9.0'
}
foreach ($m in $moduleVersions.Keys) {
    Import-Module $m -RequiredVersion $moduleVersions[$m] -ErrorAction Stop
}

$customers = Import-Csv -Path $CustomerCsv
if (-not $customers -or $customers.Count -eq 0) {
    Write-Warning "No customer tenants in '$CustomerCsv'. Nothing to scan."
    return
}

$timestamp  = Get-Date -Format 'yyyyMMdd_HHmmss'
$reportPath = Join-Path $OutputPath "Server2016_Report_$timestamp.csv"
$errorsPath = Join-Path $OutputPath "Server2016_Report_Errors_$timestamp.csv"

$findings = [System.Collections.Generic.List[object]]::new()
$errors   = [System.Collections.Generic.List[object]]::new()

function Add-ErrorRow {
    param($Stage, $TenantId, $TenantName, $SubscriptionId, $SubscriptionName, $Message)
    $errors.Add([pscustomobject]@{
        Stage             = $Stage
        TenantId          = $TenantId
        TenantName        = $TenantName
        SubscriptionId    = $SubscriptionId
        SubscriptionName  = $SubscriptionName
        Message           = $Message
    })
}

Write-Host "`n=== Sign in to Azure PowerShell ===" -ForegroundColor Cyan
Write-Host "A native sign-in window will open (WAM broker) - this supports passkeys/Windows Hello/FIDO2," -ForegroundColor Cyan
Write-Host "unlike device-code auth, which some tenants' Conditional Access policies block outright." -ForegroundColor Cyan
Connect-AzAccount | Out-Null
$homeCtx = Get-AzContext
Write-Host "Signed in to Azure as: $($homeCtx.Account.Id)" -ForegroundColor Green
Write-Host "Customer tenants to check: $($customers.Count)" -ForegroundColor Green

$idx = 0
foreach ($customer in $customers) {
    $idx++
    $tenantLabel = if ($customer.TenantName) { $customer.TenantName } else { $customer.TenantId }
    Write-Host "`n[$idx/$($customers.Count)] Tenant: $tenantLabel ($($customer.TenantId))" -ForegroundColor Yellow

    $subs = $null
    try {
        $subs = Get-AzSubscription -TenantId $customer.TenantId -ErrorAction Stop
    } catch {
        Write-Host "  Skipped - no Azure RBAC access in this tenant: $($_.Exception.Message)" -ForegroundColor DarkGray
        Add-ErrorRow -Stage 'ListSubscriptions' -TenantId $customer.TenantId -TenantName $tenantLabel `
            -SubscriptionId '' -SubscriptionName '' -Message $_.Exception.Message
        continue
    }

    if (-not $subs -or $subs.Count -eq 0) {
        Write-Host "  No subscriptions visible in this tenant." -ForegroundColor DarkGray
        continue
    }

    foreach ($sub in $subs) {
        Write-Host "  Subscription: $($sub.Name) ($($sub.Id))" -ForegroundColor Gray
        try {
            Set-AzContext -Subscription $sub.Id -Tenant $customer.TenantId -ErrorAction Stop | Out-Null
        } catch {
            Add-ErrorRow -Stage 'SetContext' -TenantId $customer.TenantId -TenantName $tenantLabel `
                -SubscriptionId $sub.Id -SubscriptionName $sub.Name -Message $_.Exception.Message
            continue
        }

        $kql = @"
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend osType = tostring(properties.storageProfile.osDisk.osType)
| extend imgPublisher = tostring(properties.storageProfile.imageReference.publisher)
| extend imgOffer = tostring(properties.storageProfile.imageReference.offer)
| extend imgSku = tostring(properties.storageProfile.imageReference.sku)
| extend powerState = tostring(properties.extended.instanceView.powerState.displayStatus)
| project name, resourceGroup, location, osType, imgPublisher, imgOffer, imgSku, powerState, id
"@
        try {
            $vms = Search-AzGraph -Query $kql -Subscription $sub.Id -ErrorAction Stop
        } catch {
            Add-ErrorRow -Stage 'ResourceGraphQuery' -TenantId $customer.TenantId -TenantName $tenantLabel `
                -SubscriptionId $sub.Id -SubscriptionName $sub.Name -Message $_.Exception.Message
            continue
        }

        foreach ($vm in $vms) {
            $isKnown2016 = $vm.imgSku -match '2016' -or $vm.imgOffer -match '2016'
            $isWindowsUnknown = ($vm.osType -eq 'Windows') -and -not $isKnown2016 -and
                                 ([string]::IsNullOrEmpty($vm.imgSku) -or $vm.imgOffer -notmatch '^WindowsServer$' -or $vm.imgSku -notmatch '^\d{4}')

            if ($isKnown2016) {
                $findings.Add([pscustomobject]@{
                    TenantId          = $customer.TenantId
                    TenantName        = $tenantLabel
                    SubscriptionId    = $sub.Id
                    SubscriptionName  = $sub.Name
                    ResourceGroup     = $vm.resourceGroup
                    VMName            = $vm.name
                    Location          = $vm.location
                    PowerState        = $vm.powerState
                    DetectedOS        = "$($vm.imgOffer) $($vm.imgSku)"
                    DetectionMethod   = 'ImageReference'
                })
                Write-Host "    [2016] $($vm.name) - $($vm.imgOffer) $($vm.imgSku)" -ForegroundColor Red
            }
            elseif ($isWindowsUnknown) {
                try {
                    $detail = Get-AzVM -ResourceGroupName $vm.resourceGroup -Name $vm.name -Status -ErrorAction Stop
                    $osName = $detail.OsName
                    if ($osName -match '2016') {
                        $findings.Add([pscustomobject]@{
                            TenantId          = $customer.TenantId
                            TenantName        = $tenantLabel
                            SubscriptionId    = $sub.Id
                            SubscriptionName  = $sub.Name
                            ResourceGroup     = $vm.resourceGroup
                            VMName            = $vm.name
                            Location          = $vm.location
                            PowerState        = $vm.powerState
                            DetectedOS        = $osName
                            DetectionMethod   = 'InstanceView'
                        })
                        Write-Host "    [2016] $($vm.name) - $osName (instance view)" -ForegroundColor Red
                    }
                } catch {
                    Add-ErrorRow -Stage 'InstanceViewCheck' -TenantId $customer.TenantId -TenantName $tenantLabel `
                        -SubscriptionId $sub.Id -SubscriptionName $sub.Name -Message "$($vm.name): $($_.Exception.Message)"
                }
            }
        }
    }
}

$findings | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
$errors   | Export-Csv -Path $errorsPath -NoTypeInformation -Encoding UTF8

Write-Host "`n=== Done ===" -ForegroundColor Cyan
Write-Host "GDAP customer tenants checked : $($customers.Count)" -ForegroundColor Green
Write-Host "Server 2016 VMs found         : $($findings.Count)" -ForegroundColor $(if ($findings.Count -gt 0) { 'Red' } else { 'Green' })
Write-Host "Report                        : $reportPath" -ForegroundColor Green
Write-Host "Tenants/subs skipped or errored (no Azure RBAC via GDAP yet): $($errors.Count)" -ForegroundColor Yellow
Write-Host "Errors log                    : $errorsPath" -ForegroundColor Yellow
(No Ratings Yet)