parv.ashwani 2 months ago
parent
commit
6564e19e1e
1 changed files with 1139 additions and 0 deletions
  1. 1139 0
      NSNitroExtractor.ps1

+ 1139 - 0
NSNitroExtractor.ps1

@@ -0,0 +1,1139 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+    NetScaler NITRO API Configuration Extractor
+    Connects live to a NetScaler ADC via the NITRO REST API and extracts
+    configuration objects similar to the file-based extractor.
+
+.DESCRIPTION
+    Prompts for NSIP, Username, and Password, authenticates via NITRO,
+    then lets you select a vServer and extracts all dependent objects
+    (services, monitors, policies, SSL certs, auth actions, etc.).
+
+.PARAMETER nsip
+    IP or FQDN of the NetScaler management interface. Prompted if not supplied.
+
+.PARAMETER username
+    NetScaler admin username. Prompted if not supplied.
+
+.PARAMETER password
+    NetScaler admin password as SecureString. Prompted if not supplied.
+
+.PARAMETER vserver
+    Partial or full vServer name to filter. Leave blank to list all.
+
+.PARAMETER outputFile
+    Path to save extracted config. "screen" to print only. Prompted if blank.
+
+.PARAMETER textEditor
+    Text editor to open output file after extraction.
+
+.PARAMETER useSSL
+    Use HTTPS (default). Set to $false to use HTTP (not recommended).
+
+.PARAMETER skipCertCheck
+    Skip SSL certificate validation (useful for self-signed certs on lab appliances).
+
+.PARAMETER nFactorNestingLevel
+    How many nFactor Next Factor levels to traverse (default 5).
+
+.NOTES
+    Change Log
+    ----------
+    2025 Mar - Initial release based on NITRO API v2 (NetScaler 12.x / 13.x / 14.x)
+               Supports: lb, cs, vpn, authentication, gslb vServers
+               Supports: services, serviceGroups, monitors, servers
+               Supports: SSL certs/profiles/ciphers/policies
+               Supports: rewrite, responder, appfw, cmp, cache, transform policies
+               Supports: AAA / nFactor (ldap, radius, saml, cert, tacacs, oauth, etc.)
+               Supports: GSLB sites/services
+               Supports: System settings (features, modes, HA, IPs, routes, DNS)
+#>
+
+param (
+    [string]  $nsip               = "",
+    [string]  $username           = "",
+    [securestring] $password      = $null,
+    [string]  $vserver            = "",
+    [string]  $outputFile         = "$env:USERPROFILE\Downloads\nsnitro_config.conf",
+    [string]  $textEditor         = "notepad++.exe",
+    [bool]    $useSSL             = $true,
+    [switch]  $skipCertCheck,
+    [switch]  $cswBind,
+    [int]     $nFactorNestingLevel = 5
+)
+
+Set-StrictMode -Off
+$ErrorActionPreference = "Stop"
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  TLS / Cert helpers
+# ─────────────────────────────────────────────────────────────────────────────
+if ($skipCertCheck) {
+    if ($PSVersionTable.PSVersion.Major -ge 6) {
+        # PowerShell Core / 7+
+        $script:InvokeParams = @{ SkipCertificateCheck = $true }
+    } else {
+        # Windows PowerShell 5.x – add a permissive policy once
+        if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) {
+            Add-Type @"
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+public class TrustAllCertsPolicy : ICertificatePolicy {
+    public bool CheckValidationResult(
+        ServicePoint srvPoint, X509Certificate certificate,
+        WebRequest request, int certificateProblem) { return true; }
+}
+"@
+        }
+        [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
+        $script:InvokeParams = @{}
+    }
+} else {
+    $script:InvokeParams = @{}
+}
+
+[System.Net.ServicePointManager]::SecurityProtocol =
+    [System.Net.SecurityProtocolType]::Tls12 -bor
+    [System.Net.SecurityProtocolType]::Tls13
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Prompt helpers
+# ─────────────────────────────────────────────────────────────────────────────
+function Prompt-Input ([string]$Prompt, [string]$Default = "") {
+    $val = Read-Host $Prompt
+    if (-not $val -and $Default) { return $Default }
+    return $val
+}
+
+function Get-OutputFilePath {
+    if ($IsMacOS) {
+        $f = (('tell application "SystemUIServer"' + "`n" + 'activate' + "`n" +
+               'set theName to POSIX path of (choose file name default name "nsnitro_config.conf" with prompt "Save extracted config as")' + "`n" +
+               'end tell' | osascript -s s) -split '"')[1]
+        return $f
+    }
+    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
+    $d = New-Object System.Windows.Forms.SaveFileDialog
+    $d.Title  = "Save Extracted NetScaler Config"
+    $d.Filter = "NetScaler Config (*.conf)|*.conf|All files (*.*)|*.*"
+    $d.ShowDialog() | Out-Null
+    return $d.FileName
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Credential / connection setup
+# ─────────────────────────────────────────────────────────────────────────────
+cls
+Write-Host "╔══════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
+Write-Host "║       NetScaler NITRO API Configuration Extractor       ║" -ForegroundColor Cyan
+Write-Host "╚══════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
+Write-Host ""
+
+if (-not $nsip)     { $nsip     = Prompt-Input "Enter NetScaler Management IP or FQDN" }
+if (-not $username) { $username = Prompt-Input "Enter Username" "nsroot" }
+if (-not $password) { $password = Read-Host "Enter Password" -AsSecureString }
+
+$proto    = if ($useSSL) { "https" } else { "http" }
+$baseUrl  = $proto + "://" + $nsip + "/nitro/v1"
+
+$BSTR     = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)
+$plainPwd = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
+[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
+
+# Session cookie store
+$script:Session = $null
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  NITRO Helper Functions
+# ─────────────────────────────────────────────────────────────────────────────
+
+function Invoke-Nitro {
+    param(
+        [string] $Method  = "GET",
+        [string] $Resource,
+        [string] $Action  = "",
+        [object] $Body    = $null,
+        [hashtable] $Query = @{}
+    )
+
+    $uri = "$baseUrl/config/$Resource"
+    if ($Query.Count -gt 0) {
+        $qs = ($Query.GetEnumerator() | ForEach-Object { "$($_.Key)=$([uri]::EscapeDataString($_.Value))" }) -join "&"
+        $uri += "?$qs"
+    }
+    if ($Action) { $uri += "?action=$Action" }
+
+    $headers = @{ "Content-Type" = "application/json" }
+    if ($script:AuthToken) { $headers["Cookie"] = "NITRO_AUTH_TOKEN=$($script:AuthToken)" }
+
+    $splat = @{
+        Uri             = $uri
+        Method          = $Method
+        Headers         = $headers
+        WebSession      = $script:Session
+        UseBasicParsing = $true
+    } + $script:InvokeParams
+
+    if ($Body) { $splat["Body"] = ($Body | ConvertTo-Json -Depth 10 -Compress) }
+
+    try {
+        $resp = Invoke-WebRequest @splat
+        $json = $resp.Content | ConvertFrom-Json
+        return $json
+    } catch {
+        $msg = $_.Exception.Message
+        if ($_.Exception.Response) {
+            try {
+                $stream = $_.Exception.Response.GetResponseStream()
+                $reader = New-Object System.IO.StreamReader($stream)
+                $errBody = $reader.ReadToEnd() | ConvertFrom-Json
+                $msg = "NITRO Error $($errBody.errorcode): $($errBody.message)"
+            } catch {}
+        }
+        Write-Warning "NITRO call failed [$Method $Resource]: $msg"
+        return $null
+    }
+}
+
+function Get-NitroObjects {
+    param(
+        [string]   $Resource,
+        [string]   $Filter    = "",
+        [string[]] $Attrs     = @(),
+        [int]      $PageSize  = 0
+    )
+
+    $query = @{}
+    if ($Filter) { $query["filter"] = $Filter }
+    if ($Attrs)  { $query["attrs"]  = ($Attrs -join ",") }
+    if ($PageSize -gt 0) { $query["count"] = "yes" }
+
+    $result = Invoke-Nitro -Method GET -Resource $Resource -Query $query
+    if (-not $result) { return @() }
+
+    # The resource name is the key in the response object
+    $key = $Resource -replace "/.*",""   # strip any sub-resource path
+    if ($result.PSObject.Properties[$key]) {
+        return @($result.$key)
+    }
+    return @()
+}
+
+function Get-NitroBinding {
+    param([string]$Resource, [string]$Name, [string]$BindType)
+    $enc  = [uri]::EscapeDataString($Name)
+    $path = "${Resource}/${enc}"
+    if ($BindType) { $path += "?type=$BindType" }
+    $result = Invoke-Nitro -Method GET -Resource $path
+    if (-not $result) { return @() }
+    $key = ($Resource -replace "/.*","") + "_binding"
+    if ($result.PSObject.Properties[$key]) { return @($result.$key) }
+    # some bindings return the type directly
+    $key2 = ($Resource -replace "/.*","") + "_" + $BindType + "_binding"
+    if ($result.PSObject.Properties[$key2]) { return @($result.$key2) }
+    return @()
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Login
+# ─────────────────────────────────────────────────────────────────────────────
+Write-Host "`nConnecting to $baseUrl ..." -ForegroundColor Yellow
+
+$loginBody = @{
+    login = @{
+        username = $username
+        password = $plainPwd
+        timeout  = 3600
+    }
+}
+
+# Use SessionVariable to capture cookies
+$loginUri = "$baseUrl/config/login"
+$loginHeaders = @{ "Content-Type" = "application/json" }
+$loginSplat = @{
+    Uri             = $loginUri
+    Method          = "POST"
+    Headers         = $loginHeaders
+    Body            = ($loginBody | ConvertTo-Json -Compress)
+    SessionVariable = "webSession"
+    UseBasicParsing = $true
+} + $script:InvokeParams
+
+try {
+    $loginResp = Invoke-WebRequest @loginSplat
+    $script:Session    = $webSession
+    $loginJson = $loginResp.Content | ConvertFrom-Json
+    # Some versions return a token, others just use the session cookie
+    if ($loginJson.PSObject.Properties["login"]) {
+        $script:AuthToken = $loginJson.login.sessionid
+    }
+    Write-Host "✔  Login successful." -ForegroundColor Green
+} catch {
+    Write-Host "✘  Login failed: $($_.Exception.Message)" -ForegroundColor Red
+    exit 1
+}
+
+$plainPwd = $null   # clear plain-text password from memory
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Output helpers
+# ─────────────────────────────────────────────────────────────────────────────
+$script:outputLines = [System.Collections.Generic.List[string]]::new()
+
+function Out-Line ([string]$line) {
+    $script:outputLines.Add($line)
+}
+
+function Out-Section ([string]$title) {
+    Out-Line ""
+    Out-Line "# $title"
+    Out-Line "# $("-" * $title.Length)"
+}
+
+function Write-Output-File {
+    param([string]$path)
+    $content = $script:outputLines -join "`n"
+    # UNIX line endings
+    [IO.File]::WriteAllText($path, $content)
+    Write-Host "`nConfig written to: $path" -ForegroundColor Green
+}
+
+function Format-NitroObject {
+    # Convert a NITRO object (PSCustomObject) into a NetScaler CLI-like line
+    param([string]$Cmd, [object]$Obj)
+    $line = $Cmd
+    foreach ($prop in $Obj.PSObject.Properties) {
+        $val = $prop.Value
+        if ($null -eq $val -or $val -eq "" -or $val -eq 0 -or $val -eq $false) { continue }
+        if ($val -is [array] -and $val.Count -eq 0) { continue }
+        $name = $prop.Name
+        # skip internal / metadata fields
+        if ($name -in @("__count","bindpoint","stateflag","flags","statechangetimesec",
+                        "statechangetimelarge","statechangetime","tickssincelaststatechange",
+                        "cursynfloodrate","vsvrbindsvcip","vsvrbindsvcport","policysubtype",
+                        "curstate","status","monstatcode","monstatparam1","monstatparam2",
+                        "monstatparam3","responsetime","riseapbrstatsmsgcode","lbvserver",
+                        "nodefaultbindings","translationip","translationmask","weight",
+                        "dynamicweight","dbslb","totalrequestbytes","totalresponsebytes",
+                        "curclntconnections","cursrvrconnections","surgecount","svrestablishedconn",
+                        "requestsinflight","avgsvrtttfb","curpersistencesessions")) { continue }
+
+        if ($val -is [array]) {
+            $val = $val -join ","
+        }
+        # quote values with spaces
+        if ("$val" -match "\s") { $val = "`"$val`"" }
+        $line += " -$name $val"
+    }
+    return $line
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Fetch & catalogue all main object types up front (cached hashtable)
+# ─────────────────────────────────────────────────────────────────────────────
+Write-Host "`nFetching configuration objects from NetScaler..." -ForegroundColor Yellow
+
+$cache = @{}
+
+function Fetch-Cache ([string]$Type) {
+    if (-not $cache.ContainsKey($Type)) {
+        $objs = Get-NitroObjects -Resource $Type
+        $cache[$Type] = $objs
+        Write-Host ("  Loaded {0,-40} ({1} objects)" -f $Type, $objs.Count)
+    }
+    return $cache[$Type]
+}
+
+# Pre-load common types
+$types = @(
+    "lbvserver","csvserver","vpnvserver","authenticationvserver","gslbvserver",
+    "service","servicegroup","server","lbmonitor",
+    "sslvserver","sslcertkey","sslprofile","sslcipher","sslpolicy","sslaction",
+    "rewritepolicy","rewriteaction","rewritepolicylabel",
+    "responderpolicy","responderaction","responderpolicylabel",
+    "cspolicy","csaction","cspolicylabel",
+    "appfwpolicy","appfwprofile","appfwpolicylabel",
+    "cmppolicy","cmpaction","cmppolicylabel",
+    "cachepolicy","cachecontentgroup","cacheselector","cachepolicylabel",
+    "transformpolicy","transformaction","transformprofile","transformpolicylabel",
+    "authenticationldapaction","authenticationldappolicy",
+    "authenticationradiusaction","authenticationradiuspolicy",
+    "authenticationsamlaction","authenticationsamlpolicy",
+    "authenticationcertaction","authenticationcertpolicy",
+    "authenticationtacacsaction","authenticationtacacspolicy",
+    "authenticationpolicy","authenticationpolicylabel","authenticationloginschemapolicy",
+    "authenticationloginschema","authenticationauthnprofile",
+    "vpnsessionpolicy","vpnsessionaction","vpntrafficpolicy","vpntrafficaction",
+    "tmsessionpolicy","tmsessionaction","tmtrafficpolicy","tmtrafficaction",
+    "vpnclientlessaccesspolicy","vpnclientlessaccessprofile",
+    "authorizationpolicy","authorizationpolicylabel",
+    "auditsyslogpolicy","auditsyslogaction","auditnslogpolicy","auditnslogaction",
+    "netprofile","nshttpprofile","nstcpprofile","dnssuffix","dnsprofile","dnsview",
+    "gslbsite","gslbservice","nslimitidentifier","nslimitselector",
+    "nsacl","route","nsip","vlan","interface","haparam","hanode","nsfeature","nsmode",
+    "systemparameter","systemuser","systemgroup"
+)
+
+foreach ($t in $types) {
+    $null = Fetch-Cache $t
+}
+
+Write-Host "`n✔  Object fetch complete." -ForegroundColor Green
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  vServer selection
+# ─────────────────────────────────────────────────────────────────────────────
+Write-Host ""
+
+# Collect all vServers from all types
+$allVServers = @()
+foreach ($vsType in @("lbvserver","csvserver","vpnvserver","authenticationvserver","gslbvserver")) {
+    foreach ($vs in $cache[$vsType]) {
+        $allVServers += [PSCustomObject]@{
+            Type = $vsType
+            Name = $vs.name
+            VIP  = if ($vs.PSObject.Properties["ipv46"])   { $vs.ipv46 }
+                   elseif ($vs.PSObject.Properties["ip"])  { $vs.ip }
+                   else { "" }
+            Port = if ($vs.PSObject.Properties["port"])    { $vs.port } else { "" }
+            Protocol = if ($vs.PSObject.Properties["servicetype"]) { $vs.servicetype } else { "" }
+            State = if ($vs.PSObject.Properties["curstate"]) { $vs.curstate } else { "" }
+        }
+    }
+}
+
+# Filter by $vserver if provided
+if ($vserver) {
+    $filtered = $allVServers | Where-Object { $_.Name -match [regex]::Escape($vserver) }
+} else {
+    $filtered = $allVServers
+}
+
+if ($filtered.Count -eq 0) {
+    Write-Host "No vServers found matching '$vserver'." -ForegroundColor Red
+    exit 1
+}
+
+# Add System Settings option
+$sysOption = [PSCustomObject]@{
+    Type = "sys"; Name = "** System Settings **"; VIP = ""; Port = ""; Protocol = ""; State = ""
+}
+$selectionList = @($sysOption) + ($filtered | Sort-Object Type, Name)
+
+Write-Host "Select Virtual Server(s) to extract:`n"
+
+if ($IsMacOS) {
+    $names = $selectionList | ForEach-Object { $_.Name.Trim('"') }
+    $vsRaw = (('tell application "SystemUIServer"' + "`n" + 'activate' + "`n" +
+              'set vserver to (choose from list  {"' + ($names -join '","') + '"} with prompt "Cmd+Select Multiple vServers" with multiple selections allowed)' + "`n" +
+              'end tell' | osascript -s s) -replace ', ',',')
+    $selectedNames = [regex]::Matches($vsRaw,'(?:([\w\s\.\*\-]+))') | ForEach-Object { $_.Value }
+    $selectedVServers = $selectionList | Where-Object { $selectedNames -contains $_.Name.Trim('"') }
+} else {
+    $selectedVServers = $selectionList | Out-GridView -Title "Ctrl+Select Multiple Virtual Servers to extract" -PassThru
+}
+
+if (-not $selectedVServers) {
+    Write-Host "No selection made. Exiting." -ForegroundColor Yellow; exit 0
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Output file prompt
+# ─────────────────────────────────────────────────────────────────────────────
+if (-not $outputFile) { $outputFile = Get-OutputFilePath }
+if (-not $outputFile) { $outputFile = "screen" }
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Extraction Engine
+# ─────────────────────────────────────────────────────────────────────────────
+
+# Lookup helpers – find object in cache by name
+function Get-ObjectByName ([string]$Type, [string]$Name) {
+    return $cache[$Type] | Where-Object { $_.name -eq $Name }
+}
+
+function Get-BindingsFor ([string]$BindResource, [string]$Name) {
+    # e.g. BindResource = "lbvserver_service_binding"  Name = "vs1"
+    $enc = [uri]::EscapeDataString($Name)
+    $result = Invoke-Nitro -Method GET -Resource "${BindResource}/${enc}"
+    if (-not $result) { return @() }
+    if ($result.PSObject.Properties[$BindResource]) {
+        return @($result.$BindResource)
+    }
+    return @()
+}
+
+# ── Write object config lines ─────────────────────────────────────────────────
+function Write-ObjectSection ([string]$Header, [string]$Type, [string[]]$Names, [string]$ExplainText="") {
+    if (-not $Names -or $Names.Count -eq 0) { return }
+    $uniqueNames = $Names | Select-Object -Unique
+    Out-Section $Header
+    foreach ($name in $uniqueNames) {
+        $obj = Get-ObjectByName $Type $name
+        if ($obj) {
+            Out-Line (Format-NitroObject "add $($Type -replace 'vserver','vServer')" $obj)
+        } else {
+            Out-Line "# [not found in cache] $Type $name"
+        }
+    }
+    if ($ExplainText) { Out-Line "# *** $ExplainText" }
+    Out-Line ""
+}
+
+# ── SSL bindings helper ───────────────────────────────────────────────────────
+function Get-SSLObjectsForVS ([string]$VSName, [string]$VSType) {
+    # sslvserver bindings
+    $sslBindRes = "${VSType}_sslcertkey_binding"
+    $bindings   = Get-BindingsFor $sslBindRes $VSName
+    $certs = @()
+    foreach ($b in $bindings) {
+        if ($b.PSObject.Properties["certkeyname"]) { $certs += $b.certkeyname }
+    }
+
+    # ssl profile
+    $sslCfg = (Invoke-Nitro -Method GET -Resource "sslvserver/$([uri]::EscapeDataString($VSName))")
+    $profile = ""
+    if ($sslCfg -and $sslCfg.PSObject.Properties["sslvserver"]) {
+        $profile = $sslCfg.sslvserver | Select-Object -First 1 -ExpandProperty sslprofile -ErrorAction SilentlyContinue
+    }
+
+    return @{ Certs = $certs; Profile = $profile }
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Per-vServer extraction
+# ─────────────────────────────────────────────────────────────────────────────
+
+$extracted = @{
+    lbvservers             = [System.Collections.Generic.List[string]]::new()
+    csvservers             = [System.Collections.Generic.List[string]]::new()
+    vpnvservers            = [System.Collections.Generic.List[string]]::new()
+    authvservers           = [System.Collections.Generic.List[string]]::new()
+    gslbvservers           = [System.Collections.Generic.List[string]]::new()
+    services               = [System.Collections.Generic.List[string]]::new()
+    servicegroups          = [System.Collections.Generic.List[string]]::new()
+    servers                = [System.Collections.Generic.List[string]]::new()
+    monitors               = [System.Collections.Generic.List[string]]::new()
+    sslcerts               = [System.Collections.Generic.List[string]]::new()
+    sslprofiles            = [System.Collections.Generic.List[string]]::new()
+    sslciphers             = [System.Collections.Generic.List[string]]::new()
+    sslpolicies            = [System.Collections.Generic.List[string]]::new()
+    sslactions             = [System.Collections.Generic.List[string]]::new()
+    rewritepolicies        = [System.Collections.Generic.List[string]]::new()
+    rewriteactions         = [System.Collections.Generic.List[string]]::new()
+    rewritepolicylabels    = [System.Collections.Generic.List[string]]::new()
+    responderpolicies      = [System.Collections.Generic.List[string]]::new()
+    responderactions       = [System.Collections.Generic.List[string]]::new()
+    responderpolicylabels  = [System.Collections.Generic.List[string]]::new()
+    cspolicies             = [System.Collections.Generic.List[string]]::new()
+    csactions              = [System.Collections.Generic.List[string]]::new()
+    cspolicylabels         = [System.Collections.Generic.List[string]]::new()
+    appfwpolicies          = [System.Collections.Generic.List[string]]::new()
+    appfwprofiles          = [System.Collections.Generic.List[string]]::new()
+    cmppolicies            = [System.Collections.Generic.List[string]]::new()
+    cachepolicies          = [System.Collections.Generic.List[string]]::new()
+    transformpolicies      = [System.Collections.Generic.List[string]]::new()
+    transformactions       = [System.Collections.Generic.List[string]]::new()
+    transformprofiles      = [System.Collections.Generic.List[string]]::new()
+    ldapactions            = [System.Collections.Generic.List[string]]::new()
+    ldappolicies           = [System.Collections.Generic.List[string]]::new()
+    radiusactions          = [System.Collections.Generic.List[string]]::new()
+    radiuspolicies         = [System.Collections.Generic.List[string]]::new()
+    samlactions            = [System.Collections.Generic.List[string]]::new()
+    samlidppolicies        = [System.Collections.Generic.List[string]]::new()
+    certactions            = [System.Collections.Generic.List[string]]::new()
+    certpolicies           = [System.Collections.Generic.List[string]]::new()
+    tacacsactions          = [System.Collections.Generic.List[string]]::new()
+    tacacspolicies         = [System.Collections.Generic.List[string]]::new()
+    authpolicies           = [System.Collections.Generic.List[string]]::new()
+    authpolicylabels       = [System.Collections.Generic.List[string]]::new()
+    loginschemas           = [System.Collections.Generic.List[string]]::new()
+    loginschemapolicies    = [System.Collections.Generic.List[string]]::new()
+    authnprofiles          = [System.Collections.Generic.List[string]]::new()
+    vpnsessionpolicies     = [System.Collections.Generic.List[string]]::new()
+    vpnsessionactions      = [System.Collections.Generic.List[string]]::new()
+    vpntrafficpolicies     = [System.Collections.Generic.List[string]]::new()
+    vpntrafficactions      = [System.Collections.Generic.List[string]]::new()
+    auditsyslogpolicies    = [System.Collections.Generic.List[string]]::new()
+    auditsyslogactions     = [System.Collections.Generic.List[string]]::new()
+    auditnslogpolicies     = [System.Collections.Generic.List[string]]::new()
+    auditnslogactions      = [System.Collections.Generic.List[string]]::new()
+    authorizationpolicies  = [System.Collections.Generic.List[string]]::new()
+    netprofiles            = [System.Collections.Generic.List[string]]::new()
+    tcpprofiles            = [System.Collections.Generic.List[string]]::new()
+    httpprofiles           = [System.Collections.Generic.List[string]]::new()
+    gslbsites              = [System.Collections.Generic.List[string]]::new()
+    gslbservices           = [System.Collections.Generic.List[string]]::new()
+    doSys                  = $false
+}
+
+function Add-Unique ([string]$Key, [string]$Value) {
+    if ($Value -and -not $extracted[$Key].Contains($Value)) {
+        $extracted[$Key].Add($Value)
+    }
+}
+
+# ── Pull bindings for an LB vServer ──────────────────────────────────────────
+function Process-LBvServer ([string]$Name) {
+    Add-Unique "lbvservers" $Name
+    Write-Host "  Processing LB vServer: $Name"
+
+    # Service bindings
+    $svcBinds = Get-BindingsFor "lbvserver_service_binding" $Name
+    foreach ($b in $svcBinds) {
+        if ($b.PSObject.Properties["servicename"]) {
+            $sn = $b.servicename; Add-Unique "services" $sn
+            Process-Service $sn
+        }
+    }
+    # ServiceGroup bindings
+    $sgBinds = Get-BindingsFor "lbvserver_servicegroup_binding" $Name
+    foreach ($b in $sgBinds) {
+        if ($b.PSObject.Properties["servicegroupname"]) {
+            $sg = $b.servicegroupname; Add-Unique "servicegroups" $sg
+            Process-ServiceGroup $sg
+        }
+    }
+    # Policies
+    $polBinds = Get-BindingsFor "lbvserver_rewritepolicy_binding"  $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-RewritePolicy  $b.policyname } }
+    $polBinds = Get-BindingsFor "lbvserver_responderpolicy_binding" $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-ResponderPolicy $b.policyname } }
+    $polBinds = Get-BindingsFor "lbvserver_appfwpolicy_binding"    $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-AppFWPolicy     $b.policyname } }
+    $polBinds = Get-BindingsFor "lbvserver_cmppolicy_binding"      $Name; foreach ($b in $polBinds) { if ($b.policyname) { Add-Unique "cmppolicies" $b.policyname } }
+    $polBinds = Get-BindingsFor "lbvserver_transformpolicy_binding" $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-TransformPolicy $b.policyname } }
+    $polBinds = Get-BindingsFor "lbvserver_auditsyslogpolicy_binding" $Name; foreach ($b in $polBinds) { if ($b.policyname) { Add-Unique "auditsyslogpolicies" $b.policyname } }
+    $polBinds = Get-BindingsFor "lbvserver_sslpolicy_binding"      $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-SSLPolicy $b.policyname } }
+    # SSL
+    $ssl = Get-SSLObjectsForVS $Name "lbvserver"
+    foreach ($c in $ssl.Certs)    { Add-Unique "sslcerts"    $c }
+    if ($ssl.Profile) { Add-Unique "sslprofiles" $ssl.Profile }
+    # Profiles
+    $obj = Get-ObjectByName "lbvserver" $Name
+    if ($obj) {
+        if ($obj.PSObject.Properties["netprofile"]      -and $obj.netprofile)      { Add-Unique "netprofiles"  $obj.netprofile }
+        if ($obj.PSObject.Properties["tcpprofilename"]  -and $obj.tcpprofilename)  { Add-Unique "tcpprofiles"  $obj.tcpprofilename }
+        if ($obj.PSObject.Properties["httpprofilename"] -and $obj.httpprofilename) { Add-Unique "httpprofiles" $obj.httpprofilename }
+        # auth
+        if ($obj.PSObject.Properties["authnvsname"] -and $obj.authnvsname) { Process-AuthVServer $obj.authnvsname }
+    }
+}
+
+function Process-Service ([string]$Name) {
+    $obj = Get-ObjectByName "service" $Name
+    if (-not $obj) { return }
+    if ($obj.PSObject.Properties["servername"] -and $obj.servername) { Add-Unique "servers" $obj.servername }
+    # Monitors
+    $mBinds = Get-BindingsFor "service_lbmonitor_binding" $Name
+    foreach ($b in $mBinds) { if ($b.PSObject.Properties["monitor_name"]) { Add-Unique "monitors" $b.monitor_name } }
+    if ($obj.PSObject.Properties["sslprofile"] -and $obj.sslprofile) { Add-Unique "sslprofiles" $obj.sslprofile }
+}
+
+function Process-ServiceGroup ([string]$Name) {
+    $obj = Get-ObjectByName "servicegroup" $Name
+    if (-not $obj) { return }
+    # Members
+    $mems = Get-BindingsFor "servicegroup_servicegroupmember_binding" $Name
+    foreach ($m in $mems) {
+        if ($m.PSObject.Properties["servername"] -and $m.servername) { Add-Unique "servers" $m.servername }
+    }
+    # Monitors
+    $mBinds = Get-BindingsFor "servicegroup_lbmonitor_binding" $Name
+    foreach ($b in $mBinds) { if ($b.PSObject.Properties["monitor_name"]) { Add-Unique "monitors" $b.monitor_name } }
+    if ($obj.PSObject.Properties["sslprofile"] -and $obj.sslprofile) { Add-Unique "sslprofiles" $obj.sslprofile }
+}
+
+function Process-CSvServer ([string]$Name) {
+    Add-Unique "csvservers" $Name
+    Write-Host "  Processing CS vServer: $Name"
+    $polBinds = Get-BindingsFor "csvserver_cspolicy_binding" $Name
+    foreach ($b in $polBinds) {
+        if ($b.PSObject.Properties["policyname"] -and $b.policyname) {
+            Add-Unique "cspolicies" $b.policyname
+            # Get action -> target LB
+            $pol = Get-ObjectByName "cspolicy" $b.policyname
+            if ($pol -and $pol.PSObject.Properties["action"] -and $pol.action) {
+                Add-Unique "csactions" $pol.action
+                $act = Get-ObjectByName "csaction" $pol.action
+                if ($act) {
+                    if ($act.PSObject.Properties["targetlbvserver"] -and $act.targetlbvserver) { Process-LBvServer $act.targetlbvserver }
+                    if ($act.PSObject.Properties["targetvserver"]   -and $act.targetvserver)   {
+                        # Could be vpn or auth
+                        if ($cache["vpnvserver"]  | Where-Object { $_.name -eq $act.targetvserver }) { Process-VPNvServer $act.targetvserver }
+                        if ($cache["authenticationvserver"] | Where-Object { $_.name -eq $act.targetvserver }) { Process-AuthVServer $act.targetvserver }
+                    }
+                }
+            }
+            if ($b.PSObject.Properties["targetlbvserver"] -and $b.targetlbvserver) { Process-LBvServer $b.targetlbvserver }
+        }
+    }
+    # Default LB
+    $obj = Get-ObjectByName "csvserver" $Name
+    if ($obj -and $obj.PSObject.Properties["lbvserver"] -and $obj.lbvserver) { Process-LBvServer $obj.lbvserver }
+    # Policies on the CS vServer itself
+    $polBinds = Get-BindingsFor "csvserver_rewritepolicy_binding"  $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-RewritePolicy  $b.policyname } }
+    $polBinds = Get-BindingsFor "csvserver_responderpolicy_binding" $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-ResponderPolicy $b.policyname } }
+    $polBinds = Get-BindingsFor "csvserver_auditsyslogpolicy_binding" $Name; foreach ($b in $polBinds) { if ($b.policyname) { Add-Unique "auditsyslogpolicies" $b.policyname } }
+    $polBinds = Get-BindingsFor "csvserver_sslpolicy_binding"      $Name; foreach ($b in $polBinds) { if ($b.policyname) { Process-SSLPolicy $b.policyname } }
+    $ssl = Get-SSLObjectsForVS $Name "csvserver"
+    foreach ($c in $ssl.Certs)    { Add-Unique "sslcerts"    $c }
+    if ($ssl.Profile) { Add-Unique "sslprofiles" $ssl.Profile }
+    if ($obj -and $obj.PSObject.Properties["netprofile"] -and $obj.netprofile) { Add-Unique "netprofiles" $obj.netprofile }
+}
+
+function Process-VPNvServer ([string]$Name) {
+    Add-Unique "vpnvservers" $Name
+    Write-Host "  Processing VPN/Gateway vServer: $Name"
+    $polBinds = Get-BindingsFor "vpnvserver_vpnsessionpolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-VPNSessionPolicy $b.policy } }
+    $polBinds = Get-BindingsFor "vpnvserver_authenticationldappolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-LDAPPolicy $b.policy } }
+    $polBinds = Get-BindingsFor "vpnvserver_authenticationradiuspolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-RADIUSPolicy $b.policy } }
+    $polBinds = Get-BindingsFor "vpnvserver_authenticationsamlpolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Add-Unique "samlidppolicies" $b.policy } }
+    $polBinds = Get-BindingsFor "vpnvserver_responderpolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-ResponderPolicy $b.policy } }
+    $polBinds = Get-BindingsFor "vpnvserver_rewritepolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-RewritePolicy $b.policy } }
+    $polBinds = Get-BindingsFor "vpnvserver_authenticationpolicy_binding" $Name
+    foreach ($b in $polBinds) {
+        if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-AuthPolicy $b.policy }
+        if ($b.PSObject.Properties["nextfactor"] -and $b.nextfactor) { Process-AuthPolicyLabel $b.nextfactor }
+    }
+    $ssl = Get-SSLObjectsForVS $Name "vpnvserver"
+    foreach ($c in $ssl.Certs)    { Add-Unique "sslcerts"    $c }
+    if ($ssl.Profile) { Add-Unique "sslprofiles" $ssl.Profile }
+    $obj = Get-ObjectByName "vpnvserver" $Name
+    if ($obj -and $obj.PSObject.Properties["authnprofile"] -and $obj.authnprofile) { Process-AuthnProfile $obj.authnprofile }
+}
+
+function Process-AuthVServer ([string]$Name) {
+    Add-Unique "authvservers" $Name
+    Write-Host "  Processing AAA/Auth vServer: $Name"
+    $polBinds = Get-BindingsFor "authenticationvserver_authenticationldappolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-LDAPPolicy $b.policy } }
+    $polBinds = Get-BindingsFor "authenticationvserver_authenticationradiuspolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-RADIUSPolicy $b.policy } }
+    $polBinds = Get-BindingsFor "authenticationvserver_authenticationpolicy_binding" $Name
+    foreach ($b in $polBinds) {
+        if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-AuthPolicy $b.policy }
+        if ($b.PSObject.Properties["nextfactor"] -and $b.nextfactor) { Process-AuthPolicyLabel $b.nextfactor }
+    }
+    $polBinds = Get-BindingsFor "authenticationvserver_authenticationloginschemapolicy_binding" $Name
+    foreach ($b in $polBinds) { if ($b.PSObject.Properties["policy"] -and $b.policy) { Process-LoginSchemaPolicy $b.policy } }
+    $ssl = Get-SSLObjectsForVS $Name "authenticationvserver"
+    foreach ($c in $ssl.Certs)    { Add-Unique "sslcerts"    $c }
+    if ($ssl.Profile) { Add-Unique "sslprofiles" $ssl.Profile }
+}
+
+function Process-GSLBvServer ([string]$Name) {
+    Add-Unique "gslbvservers" $Name
+    Write-Host "  Processing GSLB vServer: $Name"
+    $svcBinds = Get-BindingsFor "gslbvserver_gslbservice_binding" $Name
+    foreach ($b in $svcBinds) {
+        if ($b.PSObject.Properties["servicename"] -and $b.servicename) {
+            Add-Unique "gslbservices" $b.servicename
+            $svc = Get-ObjectByName "gslbservice" $b.servicename
+            if ($svc -and $svc.PSObject.Properties["sitename"] -and $svc.sitename) { Add-Unique "gslbsites" $svc.sitename }
+        }
+    }
+    $ssl = Get-SSLObjectsForVS $Name "gslbvserver"
+    foreach ($c in $ssl.Certs)    { Add-Unique "sslcerts"    $c }
+    if ($ssl.Profile) { Add-Unique "sslprofiles" $ssl.Profile }
+}
+
+function Process-RewritePolicy ([string]$Name) {
+    Add-Unique "rewritepolicies" $Name
+    $obj = Get-ObjectByName "rewritepolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "rewriteactions" $obj.action }
+}
+
+function Process-ResponderPolicy ([string]$Name) {
+    Add-Unique "responderpolicies" $Name
+    $obj = Get-ObjectByName "responderpolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "responderactions" $obj.action }
+}
+
+function Process-AppFWPolicy ([string]$Name) {
+    Add-Unique "appfwpolicies" $Name
+    $obj = Get-ObjectByName "appfwpolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["profilename"] -and $obj.profilename) { Add-Unique "appfwprofiles" $obj.profilename }
+}
+
+function Process-TransformPolicy ([string]$Name) {
+    Add-Unique "transformpolicies" $Name
+    $obj = Get-ObjectByName "transformpolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) {
+        Add-Unique "transformactions" $obj.action
+        $act = Get-ObjectByName "transformaction" $obj.action
+        if ($act -and $act.PSObject.Properties["profilename"] -and $act.profilename) { Add-Unique "transformprofiles" $act.profilename }
+    }
+}
+
+function Process-SSLPolicy ([string]$Name) {
+    Add-Unique "sslpolicies" $Name
+    $obj = Get-ObjectByName "sslpolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "sslactions" $obj.action }
+}
+
+function Process-LDAPPolicy ([string]$Name) {
+    Add-Unique "ldappolicies" $Name
+    $obj = Get-ObjectByName "authenticationldappolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "ldapactions" $obj.action }
+}
+
+function Process-RADIUSPolicy ([string]$Name) {
+    Add-Unique "radiuspolicies" $Name
+    $obj = Get-ObjectByName "authenticationradiuspolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "radiusactions" $obj.action }
+}
+
+function Process-AuthPolicy ([string]$Name) {
+    Add-Unique "authpolicies" $Name
+    $obj = Get-ObjectByName "authenticationpolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) {
+        $action = $obj.action
+        # Detect action type by searching sub-caches
+        foreach ($atype in @("authenticationldapaction","authenticationradiusaction",
+                              "authenticationsamlaction","authenticationcertaction",
+                              "authenticationtacacsaction")) {
+            if ($cache[$atype] | Where-Object { $_.name -eq $action }) {
+                switch ($atype) {
+                    "authenticationldapaction"   { Add-Unique "ldapactions"    $action }
+                    "authenticationradiusaction" { Add-Unique "radiusactions"  $action }
+                    "authenticationsamlaction"   { Add-Unique "samlactions"    $action }
+                    "authenticationcertaction"   { Add-Unique "certactions"    $action }
+                    "authenticationtacacsaction" { Add-Unique "tacacsactions"  $action }
+                }
+            }
+        }
+    }
+}
+
+function Process-AuthPolicyLabel ([string]$Name) {
+    if ($extracted["authpolicylabels"].Contains($Name)) { return }
+    Add-Unique "authpolicylabels" $Name
+    Write-Host "    Processing Auth Policy Label: $Name"
+    $binds = Get-BindingsFor "authenticationpolicylabel_authenticationpolicy_binding" $Name
+    foreach ($b in $binds) {
+        if ($b.PSObject.Properties["policyname"] -and $b.policyname) { Process-AuthPolicy $b.policyname }
+        if ($b.PSObject.Properties["nextfactor"]  -and $b.nextfactor)  { Process-AuthPolicyLabel $b.nextfactor }
+    }
+}
+
+function Process-LoginSchemaPolicy ([string]$Name) {
+    Add-Unique "loginschemapolicies" $Name
+    $obj = Get-ObjectByName "authenticationloginschemapolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "loginschemas" $obj.action }
+}
+
+function Process-VPNSessionPolicy ([string]$Name) {
+    Add-Unique "vpnsessionpolicies" $Name
+    $obj = Get-ObjectByName "vpnsessionpolicy" $Name
+    if ($obj -and $obj.PSObject.Properties["action"] -and $obj.action) { Add-Unique "vpnsessionactions" $obj.action }
+}
+
+function Process-AuthnProfile ([string]$Name) {
+    Add-Unique "authnprofiles" $Name
+    $obj = Get-ObjectByName "authenticationauthnprofile" $Name
+    if ($obj -and $obj.PSObject.Properties["authnvsname"] -and $obj.authnvsname) { Process-AuthVServer $obj.authnvsname }
+}
+
+# ── Dispatch selected vServers ────────────────────────────────────────────────
+Write-Host "`nExtracting configuration for selected vServers..." -ForegroundColor Yellow
+
+foreach ($sel in $selectedVServers) {
+    switch ($sel.Type) {
+        "lbvserver"             { Process-LBvServer   $sel.Name }
+        "csvserver"             { Process-CSvServer   $sel.Name }
+        "vpnvserver"            { Process-VPNvServer  $sel.Name }
+        "authenticationvserver" { Process-AuthVServer $sel.Name }
+        "gslbvserver"           { Process-GSLBvServer $sel.Name }
+        "sys"                   { $extracted["doSys"] = $true    }
+    }
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Build Output
+# ─────────────────────────────────────────────────────────────────────────────
+Write-Host "`nBuilding output..." -ForegroundColor Yellow
+
+$vsNames = ($selectedVServers | Where-Object { $_.Type -ne "sys" } | ForEach-Object { $_.Name }) -join ", "
+Out-Line "# NetScaler NITRO Config Extract"
+Out-Line "# NSIP       : $nsip"
+Out-Line "# Extracted  : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
+Out-Line "# vServers   : $vsNames"
+Out-Line ""
+
+# ── Helper to emit all objects of a type ─────────────────────────────────────
+function Emit-Objects ([string]$SectionTitle, [string]$CacheType, [string[]]$NameList, [string]$Cmd, [string]$Note="") {
+    if (-not $NameList -or $NameList.Count -eq 0) { return }
+    Out-Section $SectionTitle
+    if ($Note) { Out-Line "# *** $Note" }
+    foreach ($n in ($NameList | Select-Object -Unique)) {
+        $obj = Get-ObjectByName $CacheType $n
+        if ($obj) {
+            Out-Line (Format-NitroObject $Cmd $obj)
+        } else {
+            Out-Line "# [not found in live config] $CacheType $n"
+        }
+    }
+    Out-Line ""
+}
+
+# ── System Settings ───────────────────────────────────────────────────────────
+if ($extracted["doSys"]) {
+    Out-Section "System Settings"
+
+    $sysParam = Invoke-Nitro -Method GET -Resource "systemparameter"
+    if ($sysParam -and $sysParam.PSObject.Properties["systemparameter"]) {
+        Out-Line (Format-NitroObject "set system parameter" $sysParam.systemparameter)
+    }
+
+    Out-Section "Enabled Features"
+    $features = Invoke-Nitro -Method GET -Resource "nsfeature"
+    if ($features -and $features.PSObject.Properties["nsfeature"]) {
+        $f = $features.nsfeature
+        $enabled = $f.PSObject.Properties | Where-Object { $_.Value -eq "YES" -or $_.Value -eq $true } | ForEach-Object { $_.Name }
+        Out-Line ("enable ns feature " + ($enabled -join " "))
+    }
+
+    Out-Section "Enabled Modes"
+    $modes = Invoke-Nitro -Method GET -Resource "nsmode"
+    if ($modes -and $modes.PSObject.Properties["nsmode"]) {
+        $m = $modes.nsmode
+        $enabledM = $m.PSObject.Properties | Where-Object { $_.Value -eq "YES" -or $_.Value -eq $true } | ForEach-Object { $_.Name }
+        Out-Line ("enable ns mode " + ($enabledM -join " "))
+    }
+
+    Out-Section "NSIP Addresses"
+    foreach ($ip in $cache["nsip"]) { Out-Line (Format-NitroObject "add ns ip" $ip) }
+
+    Out-Section "VLANs"
+    foreach ($v in $cache["vlan"]) { Out-Line (Format-NitroObject "add vlan" $v) }
+
+    Out-Section "Routes"
+    foreach ($r in $cache["route"]) { Out-Line (Format-NitroObject "add route" $r) }
+
+    Out-Section "HA Nodes"
+    foreach ($h in $cache["hanode"]) { Out-Line (Format-NitroObject "add ha node" $h) }
+
+    Out-Section "System Users"
+    foreach ($u in $cache["systemuser"]) { Out-Line (Format-NitroObject "add system user" $u) }
+
+    Out-Section "System Groups"
+    foreach ($g in $cache["systemgroup"]) { Out-Line (Format-NitroObject "add system group" $g) }
+
+    Out-Section "ACLs"
+    foreach ($a in $cache["nsacl"]) { Out-Line (Format-NitroObject "add ns acl" $a) }
+
+    Out-Line ""
+}
+
+# ── SSL ───────────────────────────────────────────────────────────────────────
+Emit-Objects "SSL Certificates"   "sslcertkey"   $extracted["sslcerts"]    "add ssl certKey"  "Certificate files must be present in /nsconfig/ssl"
+Emit-Objects "SSL Profiles"       "sslprofile"   $extracted["sslprofiles"] "add ssl profile"
+Emit-Objects "SSL Policies"       "sslpolicy"    $extracted["sslpolicies"] "add ssl policy"
+Emit-Objects "SSL Actions"        "sslaction"    $extracted["sslactions"]  "add ssl action"
+
+# ── Networking / Profiles ─────────────────────────────────────────────────────
+Emit-Objects "Net Profiles"       "netprofile"   $extracted["netprofiles"] "add netprofile"
+Emit-Objects "TCP Profiles"       "nstcpprofile" $extracted["tcpprofiles"] "add ns tcpProfile"
+Emit-Objects "HTTP Profiles"      "nshttpprofile" $extracted["httpprofiles"] "add ns httpProfile"
+
+# ── Servers, Services, Monitors ──────────────────────────────────────────────
+Emit-Objects "Servers"            "server"       $extracted["servers"]     "add server"
+Emit-Objects "Monitors"           "lbmonitor"    $extracted["monitors"]    "add lb monitor"
+Emit-Objects "Services"           "service"      $extracted["services"]    "add service"
+Emit-Objects "Service Groups"     "servicegroup" $extracted["servicegroups"] "add serviceGroup"
+
+# ── Rewrite / Responder ───────────────────────────────────────────────────────
+Emit-Objects "Rewrite Actions"    "rewriteaction"      $extracted["rewriteactions"]     "add rewrite action"
+Emit-Objects "Rewrite Policies"   "rewritepolicy"      $extracted["rewritepolicies"]    "add rewrite policy"
+Emit-Objects "Responder Actions"  "responderaction"    $extracted["responderactions"]   "add responder action"
+Emit-Objects "Responder Policies" "responderpolicy"    $extracted["responderpolicies"]  "add responder policy"
+
+# ── AppFW ─────────────────────────────────────────────────────────────────────
+Emit-Objects "AppFW Profiles"     "appfwprofile"       $extracted["appfwprofiles"]      "add appfw profile"    "Manually export/import AppFW signatures and XML schema objects"
+Emit-Objects "AppFW Policies"     "appfwpolicy"        $extracted["appfwpolicies"]      "add appfw policy"
+
+# ── Compression ───────────────────────────────────────────────────────────────
+Emit-Objects "CMP Policies"       "cmppolicy"          $extracted["cmppolicies"]        "add cmp policy"
+
+# ── Transform ─────────────────────────────────────────────────────────────────
+Emit-Objects "Transform Profiles" "transformprofile"   $extracted["transformprofiles"]  "add transform profile"
+Emit-Objects "Transform Actions"  "transformaction"    $extracted["transformactions"]   "add transform action"
+Emit-Objects "Transform Policies" "transformpolicy"    $extracted["transformpolicies"]  "add transform policy"
+
+# ── CS ────────────────────────────────────────────────────────────────────────
+Emit-Objects "CS Actions"         "csaction"           $extracted["csactions"]          "add cs action"
+Emit-Objects "CS Policies"        "cspolicy"           $extracted["cspolicies"]         "add cs policy"
+
+# ── AAA / Authentication ──────────────────────────────────────────────────────
+Emit-Objects "LDAP Actions"       "authenticationldapaction"   $extracted["ldapactions"]    "add authentication ldapAction"  "LDAP CA certs are in /nsconfig/truststore"
+Emit-Objects "LDAP Policies"      "authenticationldappolicy"   $extracted["ldappolicies"]   "add authentication ldapPolicy"
+Emit-Objects "RADIUS Actions"     "authenticationradiusaction" $extracted["radiusactions"]  "add authentication radiusAction"
+Emit-Objects "RADIUS Policies"    "authenticationradiuspolicy" $extracted["radiuspolicies"] "add authentication radiusPolicy"
+Emit-Objects "SAML Actions"       "authenticationsamlaction"   $extracted["samlactions"]    "add authentication samlAction"
+Emit-Objects "Cert Actions"       "authenticationcertaction"   $extracted["certactions"]    "add authentication certAction"
+Emit-Objects "TACACS Actions"     "authenticationtacacsaction" $extracted["tacacsactions"]  "add authentication tacacsAction"
+Emit-Objects "TACACS Policies"    "authenticationtacacspolicy" $extracted["tacacspolicies"] "add authentication tacacsPolicy"
+Emit-Objects "Adv Auth Policies"  "authenticationpolicy"       $extracted["authpolicies"]   "add authentication Policy"
+Emit-Objects "Auth Policy Labels" "authenticationpolicylabel"  $extracted["authpolicylabels"] "add authentication policylabel"
+Emit-Objects "Login Schemas"      "authenticationloginschema"  $extracted["loginschemas"]   "add authentication loginSchema"
+Emit-Objects "Login Schema Policies" "authenticationloginschemapolicy" $extracted["loginschemapolicies"] "add authentication loginSchemaPolicy"
+Emit-Objects "Authentication Profiles" "authenticationauthnprofile" $extracted["authnprofiles"] "add authentication authnProfile"
+
+# ── VPN Session ───────────────────────────────────────────────────────────────
+Emit-Objects "VPN Session Actions"   "vpnsessionaction"  $extracted["vpnsessionactions"]  "add vpn sessionAction"
+Emit-Objects "VPN Session Policies"  "vpnsessionpolicy"  $extracted["vpnsessionpolicies"] "add vpn sessionPolicy"
+Emit-Objects "VPN Traffic Actions"   "vpntrafficaction"  $extracted["vpntrafficactions"]  "add vpn trafficAction"
+Emit-Objects "VPN Traffic Policies"  "vpntrafficpolicy"  $extracted["vpntrafficpolicies"] "add vpn trafficPolicy"
+
+# ── Audit Syslog ──────────────────────────────────────────────────────────────
+Emit-Objects "Audit Syslog Actions"  "auditsyslogaction" $extracted["auditsyslogactions"]  "add audit syslogAction"
+Emit-Objects "Audit Syslog Policies" "auditsyslogpolicy" $extracted["auditsyslogpolicies"] "add audit syslogPolicy"
+Emit-Objects "Audit NSLog Actions"   "auditnslogaction"  $extracted["auditnslogactions"]   "add audit nslogAction"
+Emit-Objects "Audit NSLog Policies"  "auditnslogpolicy"  $extracted["auditnslogpolicies"]  "add audit nslogPolicy"
+
+# ── Authorization ─────────────────────────────────────────────────────────────
+Emit-Objects "Authorization Policies" "authorizationpolicy" $extracted["authorizationpolicies"] "add authorization policy"
+
+# ── GSLB ─────────────────────────────────────────────────────────────────────
+Emit-Objects "GSLB Sites"    "gslbsite"    $extracted["gslbsites"]    "add gslb site"
+Emit-Objects "GSLB Services" "gslbservice" $extracted["gslbservices"] "add gslb service"
+
+# ── Authentication vServers (AAA) ─────────────────────────────────────────────
+Emit-Objects "Authentication Virtual Servers" "authenticationvserver" $extracted["authvservers"] "add authentication vServer"
+
+# ── vServers ──────────────────────────────────────────────────────────────────
+Emit-Objects "Load Balancing Virtual Servers" "lbvserver"  $extracted["lbvservers"]  "add lb vServer"
+Emit-Objects "Content Switching Virtual Servers" "csvserver" $extracted["csvservers"] "add cs vServer"
+Emit-Objects "Citrix Gateway Virtual Servers" "vpnvserver" $extracted["vpnvservers"] "add vpn vServer"
+Emit-Objects "GSLB Virtual Servers" "gslbvserver" $extracted["gslbvservers"] "add gslb vServer"
+
+# ── vServer bindings (SSL / Policies) ────────────────────────────────────────
+Out-Section "SSL Virtual Server Bindings"
+
+function Emit-SSLvServerBindings ([string]$VSType, [System.Collections.Generic.List[string]]$VSNames) {
+    foreach ($vsName in ($VSNames | Select-Object -Unique)) {
+        Out-Line "# --- SSL bindings for $VSType $vsName ---"
+        # Cert bindings
+        $cBinds = Get-BindingsFor "${VSType}_sslcertkey_binding" $vsName
+        foreach ($b in $cBinds) {
+            if ($b.PSObject.Properties["certkeyname"] -and $b.certkeyname) {
+                $isCaCert = if ($b.PSObject.Properties["ca"] -and $b.ca) { " -CA" } else { "" }
+                Out-Line "bind ssl $VSType $vsName -certkeyName $($b.certkeyname)$isCaCert"
+            }
+        }
+        # Cipher bindings
+        $cipBinds = Get-BindingsFor "${VSType}_sslciphersuite_binding" $vsName
+        if ($cipBinds.Count -gt 0) {
+            Out-Line "unbind ssl $VSType $vsName -cipherName DEFAULT"
+            foreach ($b in $cipBinds) {
+                if ($b.PSObject.Properties["ciphername"] -and $b.ciphername) {
+                    Out-Line "bind ssl $VSType $vsName -cipherName $($b.ciphername)"
+                }
+            }
+        }
+        # SSL profile
+        $sslCfg = Invoke-Nitro -Method GET -Resource "sslvserver/$([uri]::EscapeDataString($vsName))"
+        if ($sslCfg -and $sslCfg.PSObject.Properties["sslvserver"]) {
+            $s = $sslCfg.sslvserver | Select-Object -First 1
+            if ($s -and $s.PSObject.Properties["sslprofile"] -and $s.sslprofile) {
+                Out-Line "set ssl $VSType $vsName -sslProfile $($s.sslprofile)"
+            }
+        }
+        Out-Line ""
+    }
+}
+
+Emit-SSLvServerBindings "vserver"  $extracted["lbvservers"]
+Emit-SSLvServerBindings "vserver"  $extracted["csvservers"]
+Emit-SSLvServerBindings "vserver"  $extracted["vpnvservers"]
+Emit-SSLvServerBindings "vserver"  $extracted["authvservers"]
+
+# ── Summary ───────────────────────────────────────────────────────────────────
+Out-Section "Extraction Summary"
+Out-Line "# LB vServers           : $($extracted['lbvservers'].Count)"
+Out-Line "# CS vServers           : $($extracted['csvservers'].Count)"
+Out-Line "# VPN vServers          : $($extracted['vpnvservers'].Count)"
+Out-Line "# Auth vServers         : $($extracted['authvservers'].Count)"
+Out-Line "# GSLB vServers         : $($extracted['gslbvservers'].Count)"
+Out-Line "# Services              : $($extracted['services'].Count)"
+Out-Line "# Service Groups        : $($extracted['servicegroups'].Count)"
+Out-Line "# Servers               : $($extracted['servers'].Count)"
+Out-Line "# Monitors              : $($extracted['monitors'].Count)"
+Out-Line "# SSL Certs             : $($extracted['sslcerts'].Count)"
+Out-Line "# SSL Profiles          : $($extracted['sslprofiles'].Count)"
+Out-Line "# Rewrite Policies      : $($extracted['rewritepolicies'].Count)"
+Out-Line "# Responder Policies    : $($extracted['responderpolicies'].Count)"
+Out-Line "# AppFW Policies        : $($extracted['appfwpolicies'].Count)"
+Out-Line "# Auth Policies (Adv)   : $($extracted['authpolicies'].Count)"
+Out-Line "# LDAP Actions          : $($extracted['ldapactions'].Count)"
+Out-Line "# RADIUS Actions        : $($extracted['radiusactions'].Count)"
+Out-Line "# VPN Session Policies  : $($extracted['vpnsessionpolicies'].Count)"
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Write output
+# ─────────────────────────────────────────────────────────────────────────────
+if ($outputFile -and $outputFile -ne "screen") {
+    Write-Output-File $outputFile
+    # Try to open in a text editor - resolve full path automatically
+    $editorPath = $null
+
+    # 1. Check if the param value is already a valid full path or on PATH
+    if ($textEditor) {
+        if (Test-Path $textEditor -PathType Leaf) {
+            $editorPath = $textEditor
+        } elseif (Get-Command $textEditor -ErrorAction SilentlyContinue) {
+            $editorPath = $textEditor
+        }
+    }
+
+    # 2. Auto-detect Notepad++ in common install locations
+    if (-not $editorPath) {
+        $nppPaths = @(
+            "$env:ProgramFiles\Notepad++\notepad++.exe",
+            "${env:ProgramFiles(x86)}\Notepad++\notepad++.exe",
+            "$env:LOCALAPPDATA\Programs\Notepad++\notepad++.exe"
+        )
+        foreach ($p in $nppPaths) {
+            if (Test-Path $p -PathType Leaf) { $editorPath = $p; break }
+        }
+    }
+
+    # 3. Try VS Code
+    if (-not $editorPath) {
+        foreach ($code in @("code","code.cmd")) {
+            if (Get-Command $code -ErrorAction SilentlyContinue) { $editorPath = $code; break }
+        }
+    }
+
+    # 4. Fall back to built-in Notepad (always present on Windows)
+    if (-not $editorPath -and -not $IsMacOS) {
+        $editorPath = "notepad.exe"
+    }
+
+    if ($editorPath) {
+        Write-Host "Opening output file with: $editorPath" -ForegroundColor Cyan
+        Start-Process -FilePath $editorPath -ArgumentList "`"$outputFile`""
+    } else {
+        Write-Host "No text editor found. Output saved to: $outputFile" -ForegroundColor Yellow
+    }
+} else {
+    $script:outputLines | ForEach-Object { Write-Output $_ }
+}
+
+# ─────────────────────────────────────────────────────────────────────────────
+#  Logout
+# ─────────────────────────────────────────────────────────────────────────────
+$logoutBody = @{ logout = @{} }
+try {
+    Invoke-Nitro -Method POST -Resource "logout" -Body $logoutBody | Out-Null
+    Write-Host "✔  Logged out of NetScaler." -ForegroundColor Green
+} catch {
+    # non-fatal
+}
+
+Write-Host "`nDone." -ForegroundColor Cyan