Scripts/VPNCertEval/Build-VPNCertEvalTargets.ps1
|
<#
.SYNOPSIS Generates a STARTER "<Company>_EvalTargets.psd1" for Invoke-VPNCertEval.ps1 from a client's saved answers file (IPSEC AIO\ClientAnswers\<Abbrev>.json), optionally merging an existing User-UnitTests-style endpoint list. .DESCRIPTION The answers file knows: the RADIUS group-pair labels, the DC / RDS / file-server member lists, and every CustomAppRule's literal destination IPs + service-group names. It does NOT know vendor hostnames, subnet sample hosts, or which TCP/UDP ports a named FortiGate service group maps to. So this writes a starter you then hand-edit: * Universal[] - endpoints every connected VPN user should reach (domain controllers). * Groups[] - one bucket per RADIUS group pair, with Endpoints[] derived from the CustomAppRules whose UserGroupName matches that pair, plus a CrossCheckBlocked[] list (other groups' Allow targets this group should NOT reach - the teeth of the "what are my rights" check). * ServiceChecks{} - the port catalogue (built-in, overridden/extended by -UnitTestsPath). Every derived entry carries a Source note; anything the answers can't resolve is emitted as a Notes = 'TODO ...' string so nothing is silently dropped. .PARAMETER AnswersPath Path to ClientAnswers\<Abbrev>.json. .PARAMETER UnitTestsPath Optional. A User-UnitTests-style .ps1 whose $Endpoints / $ServiceChecks are folded in. .PARAMETER OutPath Output .psd1. Default: <answers folder>\<Company>_EvalTargets.psd1 .EXAMPLE .\Build-VPNCertEvalTargets.ps1 -AnswersPath '..\..\..\..\IPSEC AIO\ClientAnswers\ACME.json' ` -UnitTestsPath '..\..\..\..\Examples_Sources\User-UnitTests (3).ps1' #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$AnswersPath, [string]$UnitTestsPath, [string]$OutPath, [switch]$Force ) Set-StrictMode -Version 2.0 $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'VPNCertEval.Common.ps1') if (-not (Test-Path $AnswersPath)) { throw "Answers file not found: $AnswersPath" } $a = Get-Content -Path $AnswersPath -Raw | ConvertFrom-Json $company = if ($a.PSObject.Properties['Company_Name'] -and $a.Company_Name) { "$($a.Company_Name)" } else { [IO.Path]::GetFileNameWithoutExtension($AnswersPath) } if (-not $OutPath) { $OutPath = Join-Path (Split-Path -Parent (Resolve-Path $AnswersPath)) ("{0}_EvalTargets.psd1" -f ($company -replace '[^A-Za-z0-9]', '')) } if ((Test-Path $OutPath) -and -not $Force) { throw "$OutPath already exists - pass -Force to overwrite." } # --- service catalogue (+ optional merge) -------------------------------------------------------- $catalog = Get-EvalServiceCatalog $utEndpoints = $null if ($UnitTestsPath) { if (-not (Test-Path $UnitTestsPath)) { throw "UnitTestsPath not found: $UnitTestsPath" } $ut = ConvertFrom-UnitTestsFile -Path $UnitTestsPath if ($ut.ServiceChecks) { foreach ($k in $ut.ServiceChecks.Keys) { $catalog[$k] = $ut.ServiceChecks[$k] } } $utEndpoints = $ut.Endpoints } # --- helper: attach an endpoint hashtable ------------------------------------------------------- function New-Endpoint { param([string]$EpHost, [string]$Name, [string[]]$Services, [string]$Expect = 'Allow', [string]$Source, [string]$Notes) $h = [ordered]@{ Host = $EpHost; Name = $Name; Services = @($Services); Expect = $Expect; Source = $Source } if ($Notes) { $h.Notes = $Notes } return $h } # --- Universal: domain controllers ------------------------------------------------------------ $universal = New-Object System.Collections.Generic.List[object] $dcMembers = ConvertFrom-EvalQuotedList (Get-EvalProp $a 'IKEv2_DCMembers') foreach ($dc in $dcMembers) { $universal.Add((New-Endpoint -EpHost $dc -Name "Domain Controller ($dc)" ` -Services @('DNS', 'Kerberos', 'LDAP', 'LDAPS', 'GlobalCat', 'DCE-RPC', 'Ping') ` -Expect 'Allow' -Source 'IKEv2_DCMembers')) } if ($universal.Count -eq 0) { $universal.Add((New-Endpoint -EpHost 'CHANGE-ME-DC.example.local' -Name 'Domain Controller (fill in)' ` -Services @('DNS', 'Kerberos', 'Ping') -Expect 'Allow' -Source 'placeholder' -Notes 'TODO: IKEv2_DCMembers was empty in the answers')) } # --- shared internal endpoints (RDS / file servers) - attached per-group by name match -------- $rdsMembers = ConvertFrom-EvalQuotedList (Get-EvalProp $a 'IKEv2_RDSMembers') $fsMembers = ConvertFrom-EvalQuotedList (Get-EvalProp $a 'IKEv2_FileServerMembers') # --- group pairs ----------------------------------------------------------------------------- $pairs = @(Get-EvalProp $a 'RadiusGroupPairs' @()) $appRules = @(Get-EvalProp $a 'CustomAppRules' @()) function Test-GroupMatchesRule { param($Pair, $Rule) $ruleGrp = "$(Get-EvalProp $Rule 'UserGroupName')" $names = @((Get-EvalProp $Pair 'UserGroupName'), (Get-EvalProp $Pair 'UserGroupValue'), (Get-EvalProp $Pair 'Label')) | Where-Object { $_ } foreach ($n in $names) { if ("$n" -ieq $ruleGrp) { return $true } if ($ruleGrp -and "$n" -and ($ruleGrp.ToLower().Contains("$n".ToLower()) -or "$n".ToLower().Contains($ruleGrp.ToLower()))) { return $true } } return $false } $groups = New-Object System.Collections.Generic.List[object] foreach ($p in $pairs) { $eps = New-Object System.Collections.Generic.List[object] # CustomAppRules that belong to this pair foreach ($r in $appRules) { if (-not (Test-GroupMatchesRule -Pair $p -Rule $r)) { continue } $appName = "$(Get-EvalProp $r 'AppName' 'App')" $svcNames = @() foreach ($sg in (ConvertFrom-EvalQuotedList "$(Get-EvalProp $r 'ServiceNames')")) { $svcNames += Get-EvalServiceNamesForGroup -ServiceGroupName $sg } if ($svcNames.Count -eq 0) { $svcNames = @('Ping') } $svcNames = $svcNames | Select-Object -Unique $addrs = ConvertFrom-EvalQuotedList "$(Get-EvalProp $r 'AddressMembers')" foreach ($ip in $addrs) { $note = $null if ($ip -match '^(1\.1\.1\.1|0\.0\.0\.0|255\.)') { $note = 'TODO: placeholder address in the answers - replace with a real host' } $eps.Add((New-Endpoint -EpHost $ip -Name "$appName ($ip)" -Services $svcNames -Expect 'Allow' -Source "CustomAppRules[$appName]" -Notes $note)) } foreach ($named in (ConvertFrom-EvalQuotedList "$(Get-EvalProp $r 'ExistingAddressNames')")) { $eps.Add((New-Endpoint -EpHost "RESOLVE-ME" -Name "$appName / $named" -Services $svcNames -Expect 'Allow' -Source "CustomAppRules[$appName]" -Notes "TODO: '$named' is a FortiGate address-object name - look up its IP/FQDN and set Host")) } } # RDS / file-server members for internal/corp-looking pairs $looksInternal = ("$(Get-EvalProp $p 'Label') $(Get-EvalProp $p 'UserGroupName') $(Get-EvalProp $p 'UserGroupValue')" -match 'intern|corp|internalusers|\brds\b|\brdp\b|fileserv|\bsmb\b|domain') if ($looksInternal) { foreach ($m in $rdsMembers) { $eps.Add((New-Endpoint -EpHost $m -Name "RDS host ($m)" -Services @('RDP', 'Ping') -Expect 'Allow' -Source 'IKEv2_RDSMembers')) } foreach ($m in $fsMembers) { $eps.Add((New-Endpoint -EpHost $m -Name "File server ($m)" -Services @('SMB', 'Kerberos', 'Ping') -Expect 'Allow' -Source 'IKEv2_FileServerMembers')) } } $note = $null if ($eps.Count -eq 0) { $note = 'TODO: no endpoints derived for this group - add the hosts this group is meant to reach' } $groups.Add([ordered]@{ Label = "$(Get-EvalProp $p 'Label')" UserGroupName = "$(Get-EvalProp $p 'UserGroupName')" UserGroupValue = "$(Get-EvalProp $p 'UserGroupValue')" Endpoints = $eps.ToArray() # NB: @($list-of-ordered) crashes the PS7 binder CrossCheckBlocked = @() # filled below Notes = $note }) } # --- cross-group "should be BLOCKED" expectations --------------------------------------------- $maxCross = 8 foreach ($g in $groups) { $mine = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($ep in $g.Endpoints) { $h = "$(Get-EvalProp $ep 'Host')" if ($h -and $h -ne 'RESOLVE-ME') { [void]$mine.Add($h) } } $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) $blocked = New-Object System.Collections.Generic.List[object] foreach ($other in $groups) { if ("$($other['Label'])" -eq "$($g['Label'])") { continue } foreach ($ep in $other.Endpoints) { $h = "$(Get-EvalProp $ep 'Host')" if (-not $h -or $h -eq 'RESOLVE-ME') { continue } if ($mine.Contains($h) -or $seen.Contains($h)) { continue } if ($blocked.Count -ge $maxCross) { break } [void]$seen.Add($h) $blocked.Add((New-Endpoint -EpHost $h -Name "$(Get-EvalProp $ep 'Name') [expected BLOCK for $($g['Label'])]" -Services (Get-EvalProp $ep 'Services' @('Ping')) -Expect 'Block' -Source "cross-check vs $($other['Label'])")) } if ($blocked.Count -ge $maxCross) { break } } $g.CrossCheckBlocked = $blocked.ToArray() } # --- fold in a User-UnitTests endpoint list (as extra Universal Allow entries) ---------------- if ($utEndpoints) { $haveHosts = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase) foreach ($u in $universal) { $h = "$(Get-EvalProp $u 'Host')"; if ($h) { [void]$haveHosts.Add($h) } } foreach ($k in $utEndpoints.Keys) { if ($haveHosts.Contains("$k")) { continue } $universal.Add((New-Endpoint -EpHost "$k" -Name "$($utEndpoints[$k])" -Services (Get-EvalServicesForLabelText "$($utEndpoints[$k])") -Expect 'Allow' -Source 'UnitTestsFile' -Notes 'Review: from User-UnitTests file - confirm which group(s) this applies to')) } } # --- assemble + write ---------------------------------------------------------------------------- $root = [ordered]@{ Company = $company GeneratedUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") SourceAnswers = (Split-Path -Leaf $AnswersPath) IssuingModel = "$(Get-EvalProp $a 'CA_IssuingModel')" PeerSubjectDN = "$(Get-EvalProp $a 'Cert_PeerSubjectFilter')" ServiceChecks = $catalog Universal = $universal.ToArray() Groups = $groups.ToArray() } $header = @" # ============================================================================================== # $company - VPN certificate evaluation targets (STARTER - review before use) # Generated $(Get-Date) from $(Split-Path -Leaf $AnswersPath) # # Consumed by Invoke-VPNCertEval.ps1 -TargetsPath <this file>. # # BEFORE RUNNING: # * Replace every Host = 'RESOLVE-ME' / 'CHANGE-ME-*' and any Notes = 'TODO ...' entry. # * Sanity-check each endpoint's Services[] against what that group is really scoped to on # the FortiGate - the port catalogue is a guess from the service-group NAME. # * CrossCheckBlocked[] = endpoints this group should be DENIED. Trim/extend as needed. # * Add vendor hosts / subnet sample hosts the answers file has no way to know. # ============================================================================================== "@ $body = ConvertTo-EvalPsd1 -InputObject $root Set-Content -Path $OutPath -Value ($header + $body + "`r`n") -Encoding UTF8 Write-Host "Wrote $OutPath" -ForegroundColor Green Write-Host (" Universal endpoints : {0}" -f $universal.Count) -ForegroundColor Gray Write-Host (" Groups : {0}" -f $groups.Count) -ForegroundColor Gray $todo = ([regex]::Matches($body, "TODO|RESOLVE-ME|CHANGE-ME")).Count if ($todo) { Write-Host (" Unresolved markers : {0} (search the file for TODO / RESOLVE-ME / CHANGE-ME)" -f $todo) -ForegroundColor Yellow } |