Private/Read-NSPFortiGateNpsPolicy.ps1

function Read-NSPFortiGateNpsPolicy {
    <#
    .SYNOPSIS
        Reads NPS connection request and network policies, with Fortinet VSAs, from ias.xml.
    .DESCRIPTION
        Accepts the live C:\Windows\System32\ias\ias.xml or a 'netsh nps export'
        file (same schema). Only policy and RADIUS profile nodes are read here; the
        RADIUS clients are read by Read-NSPFortiGateNpsClient, which skips their shared secrets.

        Each policy has Type (ConnectionRequest or NetworkPolicy), Sequence,
        Name, Enabled, Conditions (the raw msNPConstraint strings, ANDed),
        GroupSids (from USERNTGROUPS; any one of them satisfies that condition),
        ClientAddresses (from Client-IP-Address matches), and Vsas: the
        Fortinet-Group-Name values the policy's profile returns.

        Fortinet-Group-Name is stored in msRADIUSAnyVSA as hex "01" + vendor
        00003044 (12356) + type "01" + a length byte, followed by the literal
        ASCII name; this is the encoding NPS-Manager writes.
    #>

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

    $resolved = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).ProviderPath
    [xml]$config = Get-Content -LiteralPath $resolved -Raw
    $ias = $config.Root.Children.Microsoft_Internet_Authentication_Service.Children
    if (-not $ias) { throw "'$Path' is not an NPS configuration (no Microsoft_Internet_Authentication_Service node)." }
    $profiles = $ias.RadiusProfiles.Children

    foreach ($container in @(@('Proxy_Policies', 'ConnectionRequest'), @('NetworkPolicy', 'NetworkPolicy'))) {
        $nodes = $ias.($container[0]).Children
        if (-not $nodes) { continue }
        $policies = foreach ($node in $nodes.ChildNodes) {
            if ($node.NodeType -ne 'Element') { continue }
            $sequence = 0
            [void][int]::TryParse([string]$node.Properties.msNPSequence.'#text', [ref]$sequence)
            $conditions = @(@($node.Properties.msNPConstraint) | ForEach-Object { $_.'#text' } | Where-Object { $_ })
            $sids = @(foreach ($condition in $conditions) {
                if ($condition -match '^USERNTGROUPS\((.*)\)$') { [regex]::Matches($Matches[1], '"([^"]+)"') | ForEach-Object { $_.Groups[1].Value } }
            })
            $clients = @(foreach ($condition in $conditions) {
                if ($condition -match '^MATCH\("Client-IP-Address=([^"]+)"\)$') { $Matches[1] }
            })
            $vsas = @()
            if ($container[1] -eq 'NetworkPolicy' -and $profiles) {
                $radiusProfile = $profiles.($node.LocalName)
                if ($radiusProfile) {
                    $vsas = @(foreach ($hex in @(@($radiusProfile.Properties.msRADIUSAnyVSA) | ForEach-Object { $_.'#text' } | Where-Object { $_ })) {
                        if ($hex -match '^0100003044(01)[0-9A-Fa-f]{2}(.+)$') { $Matches[2] }
                    })
                }
            }
            [pscustomobject]@{
                Type = $container[1]
                Sequence = $sequence
                Name = [string]$node.name
                Enabled = [string]$node.Properties.Policy_Enabled.'#text' -eq '1'
                Conditions = $conditions
                GroupSids = $sids
                ClientAddresses = $clients
                Vsas = $vsas
            }
        }
        @($policies) | Sort-Object Sequence
    }
}