Modules/Public/InventoryModules/Compute/VMOperationalData.ps1

<#
.Synopsis
Operational Deep-Data for Azure Virtual Machines
 
.DESCRIPTION
This script provides deep operational data for Azure VMs including backup status,
advisor recommendations count, update compliance, and lifecycle tag extraction.
Supplement to VirtualMachine.ps1 — does not replace it.
Excel Sheet Name: VM Operational Data
 
.Link
https://github.com/thisismydemo/azure-scout/Modules/Public/InventoryModules/Compute/VMOperationalData.ps1
 
.COMPONENT
This powershell Module is part of Azure Scout (AZSC)
 
.NOTES
Version: 1.0.0
First Release Date: February 24, 2026
Authors: AzureScout Contributors
 
#>


<######## Default Parameters. Don't modify this ########>

param($SCPath, $Sub, $Intag, $Resources, $Retirements, $Task, $File, $SmaResources, $TableStyle, $Unsupported)

If ($Task -eq 'Processing')
{
    $vms = $Resources | Where-Object { $_.TYPE -eq 'microsoft.compute/virtualmachines' }

    if ($vms) {

        # Pre-load backup items from Resources (ARG type for protected items)
        $backupItems = $Resources | Where-Object {
            $_.TYPE -like 'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'
        }

        # Pre-load Advisor recommendations if present in Resources
        $advisorRecs = $Resources | Where-Object { $_.TYPE -eq 'microsoft.advisor/recommendations' }

        $tmp = foreach ($1 in $vms) {
            $ResUCount = 1
            $sub1 = $SUB | Where-Object { $_.Id -eq $1.subscriptionId }
            $data = $1.PROPERTIES
            $Tags = if (![string]::IsNullOrEmpty($1.tags.psobject.properties)) { $1.tags.psobject.properties } else { '0' }

            # ---- Extensions summary ----
            $vmExts = $Resources | Where-Object {
                $_.TYPE -eq 'microsoft.compute/virtualmachines/extensions' -and
                ($_.id -split '/')[8] -eq $1.NAME
            }
            $extCount    = if ($vmExts)  { @($vmExts).Count }                                                       else { 0 }
            $extNames    = if ($vmExts)  { ($vmExts | ForEach-Object { $_.PROPERTIES.type }) -join ', ' }           else { 'None' }
            $hasAMA      = if ($vmExts -and ($vmExts.PROPERTIES.publisher -contains 'Microsoft.Azure.Monitor'))    { 'Yes' } else { 'No' }
            $hasDefender = if ($vmExts -and ($vmExts.PROPERTIES.type -like '*MDE*' -or $vmExts.PROPERTIES.Publisher -like '*MicrosoftDefender*')) { 'Yes' } else { 'No' }

            # ---- Boot diagnostics ----
            $bootDiag    = if ($data.diagnosticsProfile.bootDiagnostics.enabled -eq $true) { 'Enabled' } else { 'Disabled' }
            $bootDiagSA  = if ($data.diagnosticsProfile.bootDiagnostics.storageUri) { $data.diagnosticsProfile.bootDiagnostics.storageUri } else { 'Managed' }

            # ---- Backup status (cross-ref from pre-loaded backup items) ----
            $backupItem     = $backupItems | Where-Object { $_.PROPERTIES.sourceResourceId -eq $1.id }
            $backupEnabled  = if ($backupItem)  { 'Yes' }                                   else { 'No' }
            $lastBackupTime = if ($backupItem)  { $backupItem.PROPERTIES.lastBackupTime }   else { 'N/A' }
            $backupVault    = if ($backupItem)  { ($backupItem.id -split '/')[8] }          else { 'N/A' }
            $backupPolicy   = if ($backupItem)  { $backupItem.PROPERTIES.policyName }       else { 'N/A' }

            # ---- Advisor recommendations (cross-ref) ----
            $vmAdvisor     = $advisorRecs | Where-Object { $_.PROPERTIES.resourceMetadata.resourceId -eq $1.id }
            $advisorCount  = if ($vmAdvisor)  { @($vmAdvisor).Count }                                          else { 0 }
            $costAdvisor   = if ($vmAdvisor)  { @($vmAdvisor | Where-Object { $_.PROPERTIES.category -eq 'Cost' }).Count }        else { 0 }
            $secAdvisor    = if ($vmAdvisor)  { @($vmAdvisor | Where-Object { $_.PROPERTIES.category -eq 'Security' }).Count }    else { 0 }

            # ---- Update compliance via REST (optional, try/catch) ----
            $pendingCritical  = 'N/A'
            $pendingImportant = 'N/A'
            $lastPatchTime    = 'N/A'
            try {
                $assessUri = "/subscriptions/$($1.subscriptionId)/resourceGroups/$($1.RESOURCEGROUP)/providers/Microsoft.Compute/virtualMachines/$($1.NAME)/assessPatches?api-version=2023-03-01"
                $assessResp = Invoke-AzRestMethod -Path $assessUri -Method POST -ErrorAction SilentlyContinue
                if ($assessResp.StatusCode -in 200, 202) {
                    $assessData    = $assessResp.Content | ConvertFrom-Json
                    $pendingCritical  = if ($assessData.criticalAndSecurityPatchCount)  { $assessData.criticalAndSecurityPatchCount }  else { 0 }
                    $pendingImportant = if ($assessData.otherPatchCount)                 { $assessData.otherPatchCount }                 else { 0 }
                    $lastPatchTime    = if ($assessData.startDateTime)                   { ([datetime]$assessData.startDateTime).ToString('yyyy-MM-dd') } else { 'N/A' }
                }
            } catch {}

            # ---- Lifecycle tags ----
            $tagEnv      = if ($1.tags.Environment)  { $1.tags.Environment }  elseif ($1.tags.environment)  { $1.tags.environment }  else { 'N/A' }
            $tagOwner    = if ($1.tags.Owner)         { $1.tags.Owner }         elseif ($1.tags.owner)        { $1.tags.owner }        else { 'N/A' }
            $tagCostCenter = if ($1.tags.CostCenter) { $1.tags.CostCenter }   elseif ($1.tags.costcenter)   { $1.tags.costcenter }   else { 'N/A' }
            $tagExpiry   = if ($1.tags.ExpirationDate){ $1.tags.ExpirationDate } elseif ($1.tags.Expiration){ $1.tags.Expiration }    else { 'N/A' }

            foreach ($Tag in $Tags) {
                $obj = @{
                    'ID'                        = $1.id;
                    'Subscription'              = $sub1.Name;
                    'Resource Group'            = $1.RESOURCEGROUP;
                    'VM Name'                   = $1.NAME;
                    'Location'                  = $1.LOCATION;
                    'Power State'               = if ($data.extended.instanceView.powerState.displayStatus) { $data.extended.instanceView.powerState.displayStatus } else { 'N/A' };
                    'Extensions Count'          = $extCount;
                    'Extensions Installed'      = $extNames;
                    'Azure Monitor Agent'       = $hasAMA;
                    'Defender Extension'        = $hasDefender;
                    'Boot Diagnostics'          = $bootDiag;
                    'Boot Diag Storage'         = $bootDiagSA;
                    'Backup Enabled'            = $backupEnabled;
                    'Last Backup Time'          = $lastBackupTime;
                    'Backup Vault'              = $backupVault;
                    'Backup Policy'             = $backupPolicy;
                    'Advisor Recs Total'        = $advisorCount;
                    'Advisor Cost Recs'         = $costAdvisor;
                    'Advisor Security Recs'     = $secAdvisor;
                    'Pending Critical Patches'  = $pendingCritical;
                    'Pending Other Patches'     = $pendingImportant;
                    'Last Patch Assessment'     = $lastPatchTime;
                    'Tag: Environment'          = $tagEnv;
                    'Tag: Owner'                = $tagOwner;
                    'Tag: Cost Center'          = $tagCostCenter;
                    'Tag: Expiration Date'      = $tagExpiry;
                    'Resource U'                = $ResUCount;
                    'Tag Name'                  = [string]$Tag.Name;
                    'Tag Value'                 = [string]$Tag.Value;
                }
                $obj
                if ($ResUCount -eq 1) { $ResUCount = 0 }
            }
        }
        $tmp
    }
}

Else
{
    if ($SmaResources) {
        $TableName = ('VMOperDataTable_' + (($SmaResources.'Resource U' | Measure-Object -Sum).Sum))
        $Style = New-ExcelStyle -HorizontalAlignment Left -AutoSize -NumberFormat '0'

        $Cond  = New-ConditionalText -ConditionalType ContainsText 'No'       -ConditionalTextColor ([System.Drawing.Color]::FromArgb(255,165,0))  -BackgroundColor ([System.Drawing.Color]::White)
        $Cond2 = New-ConditionalText -ConditionalType ContainsText 'Disabled' -ConditionalTextColor ([System.Drawing.Color]::FromArgb(255,0,0))    -BackgroundColor ([System.Drawing.Color]::White)
        $Cond3 = New-ConditionalText -ConditionalType ContainsText 'Yes'      -ConditionalTextColor ([System.Drawing.Color]::FromArgb(0,176,80))   -BackgroundColor ([System.Drawing.Color]::White)

        $Exc = New-Object System.Collections.Generic.List[System.Object]
        $Exc.Add('Subscription')
        $Exc.Add('Resource Group')
        $Exc.Add('VM Name')
        $Exc.Add('Location')
        $Exc.Add('Power State')
        $Exc.Add('Extensions Count')
        $Exc.Add('Extensions Installed')
        $Exc.Add('Azure Monitor Agent')
        $Exc.Add('Defender Extension')
        $Exc.Add('Boot Diagnostics')
        $Exc.Add('Boot Diag Storage')
        $Exc.Add('Backup Enabled')
        $Exc.Add('Last Backup Time')
        $Exc.Add('Backup Vault')
        $Exc.Add('Backup Policy')
        $Exc.Add('Advisor Recs Total')
        $Exc.Add('Advisor Cost Recs')
        $Exc.Add('Advisor Security Recs')
        $Exc.Add('Pending Critical Patches')
        $Exc.Add('Pending Other Patches')
        $Exc.Add('Last Patch Assessment')
        $Exc.Add('Tag: Environment')
        $Exc.Add('Tag: Owner')
        $Exc.Add('Tag: Cost Center')
        $Exc.Add('Tag: Expiration Date')
        $Exc.Add('Resource U')
        if ($InTag) { $Exc.Add('Tag Name'); $Exc.Add('Tag Value') }

        [PSCustomObject]$SmaResources |
        ForEach-Object { $_ } | Select-Object $Exc |
        Export-Excel -Path $File `
            -WorksheetName 'VM Operational Data' `
            -AutoSize -MaxAutoSizeRows 100 `
            -ConditionalText $Cond, $Cond2, $Cond3 `
            -TableName $TableName -TableStyle $TableStyle -Style $Style
    }
}