Modules/Private/Main/Get-AZSCSafeProperty.ps1

<#
.Synopsis
Reads a (possibly nested) property off a single object, safely under StrictMode.
 
.DESCRIPTION
PowerShell's member access ($object.Name) is unsafe under Set-StrictMode -Version Latest in a
way that is easy to miss: a $null VALUE is fine (the NoteProperty exists, it just holds
nothing), but an ABSENT property throws
 
    The property 'Name' cannot be found on this object. Verify that the property exists.
 
Real Azure API responses (Resource Graph's `properties` blob in particular) only include a
field when the resource actually has something to say about it -- an optional block like
`licenseType`, `availabilitySet` or `diagnosticsProfile` is simply missing from the JSON, not
present-and-null, on the (very common) resources that do not use it. The 176 InventoryModules
collectors were written and tested without StrictMode, so they read deep chains like
`$data.osProfile.windowsConfiguration.enableAutomaticUpdates` unconditionally; under
StrictMode the very first VM whose OS profile does not carry that block throws (AB#5667), and
because which field is missing depends on which resources a tenant actually has, the failure
is data-dependent and different on every estate -- the same shape of defect AB#5633 already
fixed for member ENUMERATION over a collection. This is its single-object counterpart.
 
This function walks a dotted path one segment at a time and returns $null the moment any
segment is missing or its value is $null, rather than letting the chain throw. It is a
read-only, side-effect-free helper -- nothing about a collector's output changes when every
field in the path is actually present.
 
.PARAMETER InputObject
The object to read from. $null is accepted and returns $null.
 
.PARAMETER Path
A dot-separated property path, e.g. 'osProfile.windowsConfiguration.enableAutomaticUpdates'.
Each segment is looked up case-insensitively, matching PowerShell's own member resolution.
 
.OUTPUTS
The value at the end of the path, or $null if $InputObject is $null, or any segment along the
way is absent, or any intermediate value is $null.
 
.EXAMPLE
Get-AZSCSafeProperty -InputObject $data -Path 'osProfile.windowsConfiguration.enableAutomaticUpdates'
 
.EXAMPLE
$data.PROPERTIES | Get-AZSCSafeProperty -Path 'storageProfile.osDisk.vhd.uri'
 
.COMPONENT
This PowerShell Module is part of Azure Scout (AZSC).
#>

function Get-AZSCSafeProperty {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, ValueFromPipeline)]
        [AllowNull()]
        $InputObject,

        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string]$Path
    )

    process {
        $Current = $InputObject

        foreach ($Segment in ($Path -split '\.')) {
            if ($null -eq $Current) { return $null }

            if ($Current -is [System.Collections.IDictionary]) {
                # .Contains rather than .ContainsKey: IDictionary guarantees the former.
                if ($Current.Contains($Segment)) { $Current = $Current[$Segment] } else { return $null }
                continue
            }

            # PSObject.Properties lookup never throws for an absent member, unlike $Current.$Segment.
            $Property = $Current.PSObject.Properties[$Segment]
            if ($Property) { $Current = $Property.Value } else { return $null }
        }

        return $Current
    }
}