Private/Resolve-NSPFortiGateReference.ps1

function Resolve-NSPFortiGateReference {
    <#
    .SYNOPSIS
        Resolves a name a policy references, following what it depends on.
    .DESCRIPTION
        Address and service groups are expanded to their leaf objects; each
        leaf's Via lists the groups passed through and Excluded marks leaves
        reached through an address group's exclude-member. Other objects are
        returned as themselves; their related references (a user group's
        members and match servers, a tunnel's client pool and peer group) are
        walked, and the resolved ones are attached as Related.

        Every object reached is added to -Used as an object key, which is what
        Export-NSPFortiGateCsv -UsedByPolicy filters on. A name found in no
        loaded section comes back with Missing set, unless it is a builtin or
        its kind is not reported. Lookup tries the policy's VDOM, then
        'global', then no VDOM (captures taken inside a VDOM context carry none).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Index,
        [Parameter(Mandatory)][ValidateSet('Address', 'Service', 'User', 'Interface')][string]$Kind,
        [Parameter(Mandatory)][AllowEmptyString()][string]$Name,
        [AllowEmptyString()][string]$Vdom = '',
        [Parameter(Mandatory)][AllowEmptyCollection()][System.Collections.Generic.HashSet[string]]$Used,
        [string[]]$Via = @(),
        [switch]$Excluded
    )

    $kinds = Get-NSPFortiGateReferenceKind
    $found = $null
    foreach ($path in $kinds[$Kind].Sections) {
        foreach ($scope in @($Vdom, 'global', '') | Select-Object -Unique) {
            $dictionary = $Index["$scope`t$path"]
            $entry = $null
            if ($null -ne $dictionary -and $dictionary.TryGetValue($Name, [ref]$entry)) {
                $found = @{ Path = $path; Vdom = $scope; Entry = $entry }
                break
            }
        }
        if ($found) { break }
    }

    $leaf = [ordered]@{ Name = $Name; Kind = $Kind; Path = $null; Vdom = $Vdom; Entry = $null; Missing = $false; Excluded = [bool]$Excluded; Via = $Via; Related = @() }
    if (-not $found) {
        $leaf.Missing = $kinds[$Kind].ReportMissing -and $kinds[$Kind].Builtin -notcontains $Name
        return [pscustomobject]$leaf
    }

    [void]$Used.Add((Get-NSPFortiGateObjectKey -Vdom $found.Vdom -Path $found.Path -Name $Name))
    $leaf.Path = $found.Path
    $leaf.Vdom = $found.Vdom
    $leaf.Entry = $found.Entry
    $settings = $found.Entry.Settings

    $group = $kinds.Groups[$found.Path]
    if ($group) {
        # A group already on the current path is a loop; stop rather than recurse forever.
        if ($Via -contains $Name) { return }
        foreach ($key in $group.Keys) {
            if (-not $settings.Contains($key)) { continue }
            foreach ($member in $settings[$key]) {
                Resolve-NSPFortiGateReference -Index $Index -Kind $group[$key] -Name $member -Vdom $found.Vdom -Used $Used -Via ($Via + $Name) -Excluded:($Excluded -or $key -eq 'exclude-member')
            }
        }
        return
    }

    $related = $kinds.Related[$found.Path]
    # Walked on every visit so each policy sees its own unresolved names; a loop stops at the repeat.
    if ($related -and $Via -notcontains $Name) {
        $leaf.Related = @(
            foreach ($key in $related.Keys) {
                if (-not $settings.Contains($key)) { continue }
                foreach ($member in $settings[$key]) {
                    Resolve-NSPFortiGateReference -Index $Index -Kind $related[$key] -Name $member -Vdom $found.Vdom -Used $Used -Via ($Via + $Name)
                }
            }
            foreach ($match in @($found.Entry.Sections | Where-Object Path -eq 'match')) {
                foreach ($rule in $match.Entries) {
                    if (-not $rule.Settings.Contains('server-name')) { continue }
                    foreach ($server in $rule.Settings['server-name']) {
                        Resolve-NSPFortiGateReference -Index $Index -Kind User -Name $server -Vdom $found.Vdom -Used $Used -Via ($Via + $Name)
                    }
                }
            }
        )
    }
    [pscustomobject]$leaf
}