Public/Get-NSPFortiGateUserInventory.ps1

function Get-NSPFortiGateUserInventory {
    <#
    .SYNOPSIS
        Local users, RADIUS and LDAP servers, and user groups (with their current members) from a
        FortiGate backup or console capture - what converting local users to RADIUS or LDAP needs.
    .DESCRIPTION
        The input is a full configuration backup (.conf) or a console capture of 'show user local',
        optionally with 'show user group', 'show user radius' and 'show user ldap'. Multi-VDOM backups
        work: every row carries the VDOM it came from.
 
        Returns one object:
          Path, Kind 'backup' when the first line is '#config-version=', else 'capture'
          DeviceName hostname from 'config system global', or from the capture's prompt
          Vdoms VDOMs that hold local users (empty when not multi-VDOM)
          Users Username, Type (password, radius, ldap, ...), Disabled, Server, Vdom
          RadiusServers Name, Address, Vdom
          LdapServers Name, Address, Vdom
          Groups Name, Members, Matched (matches RADIUS/LDAP group names), Vdom
    .PARAMETER Path
        The backup or capture file.
    .EXAMPLE
        (Get-NSPFortiGateUserInventory -Path .\FGT-backup.conf).Users | Where-Object Type -eq 'password'
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param([Parameter(Mandatory)][string]$Path)

    $tree = @(ConvertFrom-NSPFortiGateConfig -Path $Path)
    $first = [string](Get-Content -LiteralPath $Path -TotalCount 1)
    $kind = if ($first -match '^#config-version=') { 'backup' } else { 'capture' }
    $value = { param($Entry, [string]$Key, [string]$Default = '') if ($Entry.Settings.Contains($Key)) { [string](@($Entry.Settings[$Key])[0]) } else { $Default } }

    # Every section, however deep: a multi-VDOM backup nests each VDOM's sections under the entries
    # of 'config vdom' (and the parser stamps them with that VDOM).
    $allSections = [Collections.Generic.List[object]]::new()
    $pending = [Collections.Generic.Queue[object]]::new()
    foreach ($section in $tree) { $pending.Enqueue($section) }
    while ($pending.Count) {
        $section = $pending.Dequeue()
        $allSections.Add($section)
        foreach ($child in $section.Sections.ToArray()) { $pending.Enqueue($child) }
        foreach ($entry in $section.Entries.ToArray()) { foreach ($child in $entry.Sections.ToArray()) { $pending.Enqueue($child) } }
    }
    $entriesOf = { param([string]$SectionPath) foreach ($section in @($allSections.ToArray() | Where-Object Path -eq $SectionPath)) { foreach ($entry in $section.Entries.ToArray()) { [pscustomobject]@{ Entry = $entry; Vdom = [string]$section.Vdom } } } }

    $deviceName = ''
    if ($kind -eq 'backup') {
        # 'config system global' holds settings directly, not edit entries.
        $global = @($allSections.ToArray() | Where-Object Path -eq 'system global' | Select-Object -First 1)
        if ($global.Count -and $global[0].Settings.Contains('hostname')) { $deviceName = [string](@($global[0].Settings['hostname'])[0]) }
    } else {
        $prompt = Select-String -LiteralPath $Path -Pattern '^(\S+?)(?: \([^)]*\))? # ' | Select-Object -First 1
        if ($prompt) { $deviceName = $prompt.Matches[0].Groups[1].Value }
    }

    $users = @(foreach ($item in (& $entriesOf 'user local')) {
            $type = (& $value $item.Entry 'type' 'password').ToLowerInvariant()
            [pscustomobject]@{
                Username = $item.Entry.Name
                Type     = $type
                Disabled = (& $value $item.Entry 'status' 'enable') -eq 'disable'
                Server   = if ($type -in 'radius', 'ldap') { & $value $item.Entry "$type-server" } else { '' }
                Vdom     = $item.Vdom
            }
        })
    $servers = {
        param([string]$SectionPath)
        @(foreach ($item in (& $entriesOf $SectionPath)) { [pscustomobject]@{ Name = $item.Entry.Name; Address = & $value $item.Entry 'server'; Vdom = $item.Vdom } })
    }
    $groups = @(foreach ($item in (& $entriesOf 'user group')) {
            $matched = @($item.Entry.Sections.ToArray() | Where-Object Path -eq 'match' | ForEach-Object { $_.Entries.ToArray() }).Count -gt 0
            [pscustomobject]@{ Name = $item.Entry.Name; Members = @($item.Entry.Settings['member'] | Where-Object { $_ }); Matched = $matched; Vdom = $item.Vdom }
        })

    [pscustomobject]@{
        PSTypeName    = 'NSP.FortiGate.UserInventory'
        Path          = $Path
        Kind          = $kind
        DeviceName    = $deviceName
        Vdoms         = @($users | ForEach-Object Vdom | Where-Object { $_ } | Select-Object -Unique)
        Users         = $users
        RadiusServers = @(& $servers 'user radius')
        LdapServers   = @(& $servers 'user ldap')
        Groups        = $groups
    }
}