powershell max authent policies

# NetScaler Authentication Policy Analyzer
# Analyse spécialisée pour les objets AAA - Application Traffic
# v2.3 - BUGFIX: Login schemas dans labels vides correctement détectés
#
# IMPORTANT: Chain of authentication:
# Actions (define HOW) -> Policies (define WHEN) -> Bindings (define WHERE)
# Example: LDAP_Action -> Auth_Policy -> bound to AAA_vServer
#
# === CHANGELOG ===
# v2.3 (Current):
# - BUGFIX CRITIQUE: Les login schemas dans des labels sans policies 
#   sont maintenant correctement marqués comme NON utilisés (rouge)
# - FIX: Pattern corrigé pour -loginSchema (case insensitive: -login[Ss]chema)
# - FIX: Détection améliorée de nextFactor (cherche dans vserver ET policylabel)
# - FIX: Correction d'une erreur de syntaxe (accolades dupliquées)
# - Messages détaillés expliquant pourquoi un schema n'est pas utilisé :
#   * "(in label: X but label has NO policies)" - Label vide
#   * "(in label: X but policies not bound to vserver)" - Policies non bindées
#   * "(not used anywhere)" - Pas dans un label du tout
#
# v2.2:
# - CORRECTION MAJEURE: Détection complète des login schemas
# - Vérification de la chaîne complète pour les schemas via policylabel
# - Schema -> PolicyLabel -> Policy -> vServer
# - Détection des schemas utilisés via loginschema policies
# - Détection des policylabels utilisés comme nextFactor
# - Messages détaillés sur pourquoi un schema n'est pas utilisé
#
# Exemple de détection login schema:
# - lschema_Auth_Role_Error -> APL_SAML_Role_Error (label) -> AAA_AUTH-POL_EPA (policy) -> VS-AAA (vserver)
# Le script vérifie TOUTE cette chaîne pour confirmer l'utilisation
#
# v2.1:
# - Réorganisation des objets par ordre logique de chaînage
# - Profile -> vServer -> Schema/Policy -> Label -> Action
# - Chemin par défaut changé vers C:\logs\ns.conf
# - Suppression des sections non essentielles pour clarté
#
# v2.0:
# - Ajout complet de l'analyse des Actions d'authentification
# - Analyse de la chaîne complète: Action -> Policy -> Binding
# - Support des 5 types d'actions: LDAP, OAUTH, SAML, RADIUS, EPA
# - Affichage avec code couleur spécifique pour les actions (cyan)
# - Détection des policies qui utilisent des actions mais ne sont pas bindées
#
# v1.3:
# - Correction du calcul des statistiques (Total = Bound + Unbound)
# - Ajout du mode debug pour diagnostic
# - Validation automatique des calculs
#
# === FEATURES ===
# - Analyse complète de 10 types d'objets AAA
# - Détection des bindings globaux, vserver, et labels
# - Vérification de la chaîne complète pour les actions
# - Détection approfondie des login schemas (3 chemins possibles)
# - Code couleur intuitif pour visualisation rapide
# - Statistiques détaillées et validation automatique
# - Diagramme de flux d'authentification
# - Idéal pour identifier les objets à nettoyer
#
# === OBJETS ANALYSÉS (dans l'ordre du flux) ===
# Niveau 1 - Point d'entrée:
#   1. Authentication Profiles → Attachés aux VPN/LB vServers
# 
# Niveau 2 - Serveur d'authentification:
#   2. Authentication vServers → Référencés dans les profiles
#
# Niveau 3 - Configuration:
#   3. Authentication Login Schemas → Bindés aux vServers
#   4. Authentication Policies → Bindées aux vServers ou globalement
#   5. Authentication Policy Labels → Utilisés comme nextFactor
#
# Niveau 4 - Actions (endpoints):
#   6. LDAP Actions → Définissent comment authentifier
#   7. OAUTH Actions → Définissent l'auth OAuth
#   8. SAML Actions → Définissent l'auth SAML
#   9. RADIUS Actions → Définissent l'auth RADIUS
#   10. EPA Actions → Définissent les checks EPA
#
# === UTILISATION ===
# 1. Modifier $NS_CONF_PATH avec le chemin vers votre fichier ns.conf
# 2. Exécuter le script PowerShell
# 3. Analyser les résultats avec le code couleur
# 4. Activer $DEBUG_MODE = $true pour plus de détails
#
# === ATTENTION - BUG CORRIGÉ EN v2.3 ===
# Les versions précédentes marquaient incorrectement comme utilisés les
# login schemas présents dans des policy labels vides. Ce bug est corrigé.
# Un schema n'est maintenant marqué comme utilisé QUE si la chaîne complète
# est valide jusqu'à un vserver.
#
# Fonctionnalités:
# - Code couleur : VERT (bindé/utilisé), ROUGE (non bindé), GRIS (détails)
# - Support des bindings globaux (bind system global)
# - Affichage des priorités et des relations

$NS_CONF_PATH = "C:\logs\ns.conf"
$DEBUG_MODE = $false  # Set to $true to see detailed calculation info

Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host "          NetScaler Authentication (AAA - Application Traffic) Analysis" -ForegroundColor Yellow
Write-Host ("=" * 80) + "`n" -ForegroundColor Cyan

# Load configuration
function Load-NSConf {
    param([string]$FilePath)
    
    if (Test-Path $FilePath) {
        return Get-Content -Path $FilePath -Encoding UTF8
    } else {
        Write-Host "ERROR: File $FilePath not found." -ForegroundColor Red
        return @()
    }
}

$configLines = Load-NSConf -FilePath $NS_CONF_PATH
if ($configLines.Count -eq 0) {
    exit
}

Write-Host "Configuration loaded: $($configLines.Count) lines`n" -ForegroundColor Green

# ADC Interface paths
$policyPaths = @{
    # Existing authentication objects
    "authentication vserver" = "Security > AAA - Application Traffic > Authentication Virtual Servers"
    "authentication authnprofile" = "Security > AAA - Application Traffic > Authentication Profile"
    "authentication loginschema" = "Security > AAA - Application Traffic > Login Schema > Policies"
    "authentication loginschemaprofile" = "Security > AAA - Application Traffic > Login Schema > Profiles"
    "authentication policy" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Authentication Policies"
    "authentication policylabel" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Authentication Policy Labels"
    
    # Authentication Actions (for future analysis)
    "authentication ldapAction" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Actions > LDAP Actions"
    "authentication OAuthAction" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Actions > OAUTH Actions"
    "authentication samlAction" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Actions > SAML Actions"
    "authentication radiusAction" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Actions > RADIUS Actions"
    "authentication epaAction" = "Security > AAA - Application Traffic > Policies > Authentication > Advanced Policies > Actions > Authentication EPA Action"
}

# Note: All Authentication Actions are now fully analyzed in v2.0
# The script checks if actions are used by policies and if those policies are bound

function Find-DefinedObjects {
    param(
        [string[]]$ConfigLines,
        [string]$ObjectType
    )
    
    $configString = $ConfigLines -join "`n"
    
    # Adjust the pattern for different object types
    # Actions can have mixed case (ldapAction, OAuthAction, etc.)
    if ($ObjectType -match "Action$") {
        # For actions, be more flexible with case
        $actualType = $ObjectType -replace "authentication ", ""
        $pattern = "(?i)add authentication $actualType\s+(`"[^`"]+`"|\S+)"
    } else {
        $pattern = "(?i)add $ObjectType\s+(`"[^`"]+`"|\S+)"
    }
    
    $matches = [regex]::Matches($configString, $pattern)
    $objects = @{}
    
    foreach ($match in $matches) {
        $objectName = $match.Groups[1].Value.Trim('"')
        $objects[$objectName] = @{
            Name = $objectName
            Type = $ObjectType
            BoundTo = @()
            BindingDetails = @()
        }
    }
    
    return $objects
}

function Find-BoundObjects {
    param(
        [string[]]$ConfigLines,
        [string]$ObjectType,
        [hashtable]$DefinedObjects
    )
    
    $configString = $ConfigLines -join "`n"
    $boundObjects = @{}
    
    switch ($ObjectType.ToLower()) {
        "authentication policy" {
            # Bound to authentication vservers
            $directPattern = "(?is)bind authentication vserver\s+(\S+).*?-policy\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $directPattern)
            foreach ($match in $matches) {
                $vserver = $match.Groups[1].Value.Trim('"')
                $policy = $match.Groups[2].Value.Trim('"')
                if (-not $boundObjects.ContainsKey($policy)) {
                    $boundObjects[$policy] = @()
                }
                # Extract priority if available
                if ($match.Groups[0].Value -match "-priority\s+(\d+)") {
                    $priority = $Matches[1]
                    $boundObjects[$policy] += "AUTH_VSERVER: $vserver (priority: $priority)"
                } else {
                    $boundObjects[$policy] += "AUTH_VSERVER: $vserver"
                }
            }
            
            # Bound to policy labels (using -policyName)
            $labelPattern1 = "(?is)bind authentication policylabel\s+(\S+).*?-policyName\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $labelPattern1)
            foreach ($match in $matches) {
                $label = $match.Groups[1].Value.Trim('"')
                $policy = $match.Groups[2].Value.Trim('"')
                if (-not $boundObjects.ContainsKey($policy)) {
                    $boundObjects[$policy] = @()
                }
                # Extract priority if available
                if ($match.Groups[0].Value -match "-priority\s+(\d+)") {
                    $priority = $Matches[1]
                    $boundObjects[$policy] += "POLICY_LABEL: $label (priority: $priority)"
                } else {
                    $boundObjects[$policy] += "POLICY_LABEL: $label"
                }
            }
            
            # Alternative pattern for policy labels (using -policy)
            $labelPattern2 = "(?is)bind authentication policylabel\s+(\S+).*?-policy\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $labelPattern2)
            foreach ($match in $matches) {
                $label = $match.Groups[1].Value.Trim('"')
                $policy = $match.Groups[2].Value.Trim('"')
                if (-not $boundObjects.ContainsKey($policy)) {
                    $boundObjects[$policy] = @()
                }
                $bindingText = "POLICY_LABEL: $label"
                if ($match.Groups[0].Value -match "-priority\s+(\d+)") {
                    $priority = $Matches[1]
                    $bindingText = "POLICY_LABEL: $label (priority: $priority)"
                }
                if ($boundObjects[$policy] -notcontains $bindingText) {
                    $boundObjects[$policy] += $bindingText
                }
            }
            
            # Bound globally to system
            $globalPattern = "(?is)bind system global\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $globalPattern)
            foreach ($match in $matches) {
                $policy = $match.Groups[1].Value.Trim('"')
                # Check if this is actually an authentication policy
                if ($DefinedObjects.ContainsKey($policy)) {
                    if (-not $boundObjects.ContainsKey($policy)) {
                        $boundObjects[$policy] = @()
                    }
                    # Extract priority and other details
                    if ($match.Groups[0].Value -match "-priority\s+(\d+)") {
                        $priority = $Matches[1]
                        $bindingText = "GLOBAL_SYSTEM (priority: $priority)"
                        # Check for gotoPriorityExpression
                        if ($match.Groups[0].Value -match "-gotoPriorityExpression\s+(\S+)") {
                            $goto = $Matches[1]
                            $bindingText += " -> $goto"
                        }
                        $boundObjects[$policy] += $bindingText
                    } else {
                        $boundObjects[$policy] += "GLOBAL_SYSTEM"
                    }
                }
            }
        }
        
        "authentication policylabel" {
            # Policy labels are bound as nextFactor
            $pattern = "(?is)bind authentication vserver\s+(\S+).*?-nextFactor\s+(\S+)"
            $matches = [regex]::Matches($configString, $pattern)
            foreach ($match in $matches) {
                $vserver = $match.Groups[1].Value.Trim('"')
                $label = $match.Groups[2].Value.Trim('"')
                if (-not $boundObjects.ContainsKey($label)) {
                    $boundObjects[$label] = @()
                }
                $boundObjects[$label] += "AUTH_VSERVER: $vserver (as nextFactor)"
            }
            
            # Policy labels can also be in policy goto expressions
            $gotoPattern = "(?is)bind authentication policylabel\s+\S+.*?-gotoPriorityExpression\s+.*?`"([^`"]+)`""
            $matches = [regex]::Matches($configString, $gotoPattern)
            foreach ($match in $matches) {
                $label = $match.Groups[1].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($label)) {
                    if (-not $boundObjects.ContainsKey($label)) {
                        $boundObjects[$label] = @()
                    }
                    $boundObjects[$label] += "Referenced in gotoPriorityExpression"
                }
            }
        }
        
        "authentication authnprofile" {
            # Profiles are bound to VPN vservers
            $pattern = "(?is)add vpn vserver\s+(\S+).*?-authnProfile\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $pattern)
            foreach ($match in $matches) {
                $vserver = $match.Groups[1].Value.Trim('"')
                $profile = $match.Groups[2].Value.Trim('"')
                if (-not $boundObjects.ContainsKey($profile)) {
                    $boundObjects[$profile] = @()
                }
                $boundObjects[$profile] += "VPN_VSERVER: $vserver"
            }
            
            # Profiles can also be bound to LB vservers
            $lbPattern = "(?is)add lb vserver\s+(\S+).*?-authnProfile\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $lbPattern)
            foreach ($match in $matches) {
                $vserver = $match.Groups[1].Value.Trim('"')
                $profile = $match.Groups[2].Value.Trim('"')
                if (-not $boundObjects.ContainsKey($profile)) {
                    $boundObjects[$profile] = @()
                }
                $boundObjects[$profile] += "LB_VSERVER: $vserver"
            }
        }
        
        "authentication vserver" {
            # AAA vservers are referenced in authnProfiles
            $mapping = @{}
            $patternMap = "(?i)add authentication authnProfile\s+(`"([^`"]+)`"|\S+).*?-authnVsName\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $patternMap)
            foreach ($match in $matches) {
                $profile = $match.Groups[1].Value.Trim('"')
                $aaaVserver = $match.Groups[3].Value.Trim('"')
                $mapping[$profile] = $aaaVserver
            }
            
            # Find where these profiles are used
            $patternBound = "(?is)add vpn vserver\s+(\S+).*?-authnProfile\s+(`"([^`"]+)`"|\S+)"
            $matches = [regex]::Matches($configString, $patternBound)
            foreach ($match in $matches) {
                $vserver = $match.Groups[1].Value.Trim('"')
                $profile = $match.Groups[2].Value.Trim('"')
                if ($mapping.ContainsKey($profile)) {
                    $aaaVserver = $mapping[$profile]
                    if (-not $boundObjects.ContainsKey($aaaVserver)) {
                        $boundObjects[$aaaVserver] = @()
                    }
                    $boundObjects[$aaaVserver] += "VPN_VSERVER: $vserver (via profile: $profile)"
                }
            }
            
            # AAA vservers can also be referenced directly
            $directPattern = "(?is)set vpn vserver\s+(\S+).*?-authentication\s+(\S+)"
            $matches = [regex]::Matches($configString, $directPattern)
            foreach ($match in $matches) {
                $vserver = $match.Groups[1].Value.Trim('"')
                $aaaVserver = $match.Groups[2].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($aaaVserver)) {
                    if (-not $boundObjects.ContainsKey($aaaVserver)) {
                        $boundObjects[$aaaVserver] = @()
                    }
                    $boundObjects[$aaaVserver] += "VPN_VSERVER: $vserver (direct)"
                }
            }
        }
        
        "authentication loginschema" {
            # Login schemas can be used in 2 ways:
            # 1. Direct binding to vserver via loginschema policy
            # 2. Used in policylabel which must have policies that are bound to vservers
            
            # Method 1: Check for loginschema policies that use this schema
            $policyPattern = "(?is)add authentication loginschemapolicy\s+(`"[^`"]+`"|\S+).*?-action\s+(`"[^`"]+`"|\S+)"
            $policyMatches = [regex]::Matches($configString, $policyPattern)
            
            $schemaPolicies = @{} # Track which policies use which schemas
            foreach ($match in $policyMatches) {
                $policyName = $match.Groups[1].Value.Trim('"')
                $schemaName = $match.Groups[2].Value.Trim('"')
                if (-not $schemaPolicies.ContainsKey($schemaName)) {
                    $schemaPolicies[$schemaName] = @()
                }
                $schemaPolicies[$schemaName] += $policyName
            }
            
            # Now check if these policies are bound to vservers
            foreach ($schemaName in $schemaPolicies.Keys) {
                if ($DefinedObjects.ContainsKey($schemaName)) {
                    foreach ($policyName in $schemaPolicies[$schemaName]) {
                        # Check if this policy is bound to a vserver
                        $bindPattern = "(?is)bind authentication vserver\s+(\S+).*?-policy\s+$([regex]::Escape($policyName))"
                        $bindMatches = [regex]::Matches($configString, $bindPattern)
                        foreach ($bindMatch in $bindMatches) {
                            $vserver = $bindMatch.Groups[1].Value.Trim('"')
                            if (-not $boundObjects.ContainsKey($schemaName)) {
                                $boundObjects[$schemaName] = @()
                            }
                            $boundObjects[$schemaName] += "AUTH_VSERVER: $vserver (via policy: $policyName)"
                        }
                    }
                }
            }
            
            # Method 2: Check for schemas used in policy labels
            # Pattern handles both -loginSchema and -loginschema (case insensitive)
            $labelPattern = "(?is)add authentication policylabel\s+(`"[^`"]+`"|\S+).*?-login[Ss]chema\s+(`"[^`"]+`"|\S+)"
            $labelMatches = [regex]::Matches($configString, $labelPattern)
            
            foreach ($match in $labelMatches) {
                $labelName = $match.Groups[1].Value.Trim('"')
                $schemaName = $match.Groups[2].Value.Trim('"')
                
                if ($DefinedObjects.ContainsKey($schemaName)) {
                    # Check if this policylabel has policies bound to it
                    $labelPolicyPattern = "(?is)bind authentication policylabel\s+$([regex]::Escape($labelName)).*?-policyName\s+(`"[^`"]+`"|\S+)"
                    $labelPolicyMatches = [regex]::Matches($configString, $labelPolicyPattern)
                    
                    $labelHasPolicies = $false
                    $policiesInLabel = @()
                    
                    foreach ($policyMatch in $labelPolicyMatches) {
                        $policyInLabel = $policyMatch.Groups[1].Value.Trim('"')
                        $policiesInLabel += $policyInLabel
                        $labelHasPolicies = $true
                    }
                    
                    if ($labelHasPolicies) {
                        # Now check if any of these policies are bound to vservers
                        $labelUsed = $false
                        foreach ($policy in $policiesInLabel) {
                            $vserverBindPattern = "(?is)bind authentication vserver\s+(\S+).*?-policy\s+$([regex]::Escape($policy))"
                            $vserverMatches = [regex]::Matches($configString, $vserverBindPattern)
                            
                            foreach ($vMatch in $vserverMatches) {
                                $vserver = $vMatch.Groups[1].Value.Trim('"')
                                if (-not $boundObjects.ContainsKey($schemaName)) {
                                    $boundObjects[$schemaName] = @()
                                }
                                $boundObjects[$schemaName] += "POLICY_LABEL: $labelName -> POLICY: $policy -> VSERVER: $vserver"
                                $labelUsed = $true
                            }
                        }
                        
                        # If label has policies but they're not bound to vservers - DON'T mark as bound
                        if (-not $labelUsed) {
                            # Don't add to boundObjects - this is an unused chain
                            # We can track this for informational purposes but it's NOT a valid binding
                            if ($DEBUG_MODE) {
                                Write-Host "DEBUG: Schema $schemaName in label $labelName has policies but they're not bound" -ForegroundColor DarkYellow
                            }
                        }
                    } else {
                        # Label exists but has no policies - DON'T mark as bound
                        # This is definitely not a valid use of the schema
                        if ($DEBUG_MODE) {
                            Write-Host "DEBUG: Schema $schemaName in label $labelName has no policies" -ForegroundColor DarkYellow
                        }
                    }
                }
            }
            
            # Also check for any nextFactor usage of policylabels that contain schemas
            # (policylabels can be used as nextFactor even without direct policy bindings)
            foreach ($match in $labelMatches) {
                $labelName = $match.Groups[1].Value.Trim('"')
                $schemaName = $match.Groups[2].Value.Trim('"')
                
                if ($DefinedObjects.ContainsKey($schemaName)) {
                    # Check for nextFactor usage - more robust pattern
                    $nextFactorPattern = "(?is)bind authentication (?:vserver|policylabel)\s+(\S+).*?-nextFactor\s+$([regex]::Escape($labelName))"
                    $nextFactorMatches = [regex]::Matches($configString, $nextFactorPattern)
                    
                    foreach ($nfMatch in $nextFactorMatches) {
                        $vserver = $nfMatch.Groups[1].Value.Trim('"')
                        if (-not $boundObjects.ContainsKey($schemaName)) {
                            $boundObjects[$schemaName] = @()
                        }
                        $binding = "POLICY_LABEL: $labelName -> VSERVER: $vserver (as nextFactor)"
                        if ($boundObjects[$schemaName] -notcontains $binding) {
                            $boundObjects[$schemaName] += $binding
                        }
                    }
                }
            }
        }
        
        { $_ -in @("authentication ldapAction", "authentication OAuthAction", 
                   "authentication samlAction", "authentication radiusAction", 
                   "authentication epaAction") } {
            # Actions are referenced in authentication policies with -action parameter
            # 
            # Chain analysis for actions:
            # 1. Find all policies that use this action
            # 2. For each policy, check if the policy itself is bound somewhere
            # 3. Report the complete chain: Action -> Policy -> Binding location
            
            # Find policies that use this action
            # Pattern captures: policy name (group 1) and action name (group 2)
            $policyPattern = "(?is)add authentication policy\s+(`"[^`"]+`"|\S+).*?-action\s+(`"[^`"]+`"|\S+)"
            $matches = [regex]::Matches($configString, $policyPattern)
            
            foreach ($match in $matches) {
                $policyName = $match.Groups[1].Value.Trim('"')
                $actionName = $match.Groups[2].Value.Trim('"')
                
                # Check if this action is one of our defined objects
                if ($DefinedObjects.ContainsKey($actionName)) {
                    if (-not $boundObjects.ContainsKey($actionName)) {
                        $boundObjects[$actionName] = @()
                    }
                    
                    # Now check if this policy is bound somewhere
                    $policyBound = $false
                    $bindingDetails = @()
                    
                    # Check if policy is bound to vserver
                    $vserverPattern = "(?is)bind authentication vserver\s+(\S+).*?-policy\s+$([regex]::Escape($policyName))"
                    $vserverMatches = [regex]::Matches($configString, $vserverPattern)
                    foreach ($vMatch in $vserverMatches) {
                        $vserver = $vMatch.Groups[1].Value.Trim('"')
                        $bindingDetails += "via AUTH_VSERVER: $vserver"
                        $policyBound = $true
                    }
                    
                    # Check if policy is bound to label
                    $labelPattern = "(?is)bind authentication policylabel\s+(\S+).*?-policyName\s+$([regex]::Escape($policyName))"
                    $labelMatches = [regex]::Matches($configString, $labelPattern)
                    foreach ($lMatch in $labelMatches) {
                        $label = $lMatch.Groups[1].Value.Trim('"')
                        $bindingDetails += "via POLICY_LABEL: $label"
                        $policyBound = $true
                    }
                    
                    # Check if policy is bound globally
                    $globalPattern = "(?is)bind system global\s+$([regex]::Escape($policyName))"
                    if ([regex]::IsMatch($configString, $globalPattern)) {
                        $bindingDetails += "via GLOBAL_SYSTEM"
                        $policyBound = $true
                    }
                    
                    # Add to bound objects with details
                    if ($policyBound) {
                        foreach ($detail in $bindingDetails) {
                            $boundObjects[$actionName] += "POLICY: $policyName ($detail)"
                        }
                    } else {
                        $boundObjects[$actionName] += "POLICY: $policyName (policy not bound)"
                    }
                }
            }
        }
    }
    
    # Update defined objects with binding info
    foreach ($objName in $boundObjects.Keys) {
        if ($DefinedObjects.ContainsKey($objName)) {
            $DefinedObjects[$objName].BoundTo = $boundObjects[$objName]
        }
    }
    
    return $boundObjects
}

# Authentication object types to analyze (ordered by logical chain flow)
$authTypes = @(
    # 1. Entry point - Profiles bound to VPN/LB vservers
    "authentication authnprofile",
    
    # 2. Authentication vServers referenced in profiles
    "authentication vserver",
    
    # 3. Login schemas bound to vservers
    "authentication loginschema",
    
    # 4. Policies bound to vservers or globally
    "authentication policy",
    
    # 5. Policy labels used as nextFactor
    "authentication policylabel",
    
    # 6. Actions used by policies (endpoints)
    "authentication ldapAction",
    "authentication OAuthAction", 
    "authentication samlAction",
    "authentication radiusAction",
    "authentication epaAction"
)

# Note: The order follows the authentication flow from entry point to endpoints
# Profile -> vServer -> Schema/Policy -> Label -> Action

Write-Host "Starting authentication policy analysis..." -ForegroundColor Yellow
Write-Host "Objects will be analyzed in logical flow order (entry point → endpoints)" -ForegroundColor DarkGray
Write-Host ("=" * 80) -ForegroundColor DarkGray
Write-Host ""

$allPolicies = @{}
$totalDefined = 0
$totalBound = 0
$totalUnbound = 0

foreach ($authType in $authTypes) {
    Write-Progress -Activity "Analysis in progress" -Status "Processing: $authType" `
                  -PercentComplete (($authTypes.IndexOf($authType) + 1) / $authTypes.Count * 100)
    
    $defined = Find-DefinedObjects -ConfigLines $configLines -ObjectType $authType
    $bound = Find-BoundObjects -ConfigLines $configLines -ObjectType $authType -DefinedObjects $defined
    
    # Store all objects for this type
    if ($defined.Count -gt 0) {
        # IMPORTANT: Calculate the actual number of bound objects
        # $bound is a hashtable where keys are object names and values are arrays of bindings
        # We need to count how many defined objects have at least one binding
        # NOT the count of the $bound hashtable itself
        $boundCount = 0
        foreach ($obj in $defined.Keys) {
            if ($bound.ContainsKey($obj) -and $bound[$obj].Count -gt 0) {
                $boundCount++
            }
        }
        
        if ($DEBUG_MODE) {
            Write-Host "DEBUG [$authType]: Defined=$($defined.Count), Bound=$boundCount, Unbound=$($defined.Count - $boundCount)" -ForegroundColor DarkYellow
        }
        
        $allPolicies[$authType] = @{
            Defined = $defined
            Bound = $bound
            BoundCount = $boundCount
            UnboundCount = ($defined.Count - $boundCount)
        }
        
        $totalDefined += $defined.Count
        $totalBound += $boundCount
        $totalUnbound += ($defined.Count - $boundCount)
    }
}

Write-Progress -Completed -Activity "Analysis in progress"

# Display results with color coding
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "            AUTHENTICATION OBJECTS - STATUS BY LOGICAL FLOW" -ForegroundColor Yellow
Write-Host "                    (Entry Point → Configuration → Actions)" -ForegroundColor DarkGray
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host ""
Write-Host "Legend: " -NoNewline
Write-Host "[✓] Bound/Used " -ForegroundColor Green -NoNewline
Write-Host "[✓] Actions " -ForegroundColor Cyan -NoNewline
Write-Host "[X] Unbound/Unused " -ForegroundColor Red -NoNewline
Write-Host "[GLOBAL_SYSTEM] Global" -ForegroundColor Cyan
Write-Host "        Policy not bound warnings shown in " -NoNewline
Write-Host "Dark Yellow" -ForegroundColor DarkYellow
Write-Host ""

foreach ($authType in $authTypes) {
    if ($allPolicies.ContainsKey($authType)) {
        $policies = $allPolicies[$authType]
        $defined = $policies.Defined
        $bound = $policies.Bound
        $boundCount = $policies.BoundCount
        $unboundCount = $policies.UnboundCount
        
        # Get path from policyPaths, handle case sensitivity
        $path = $null
        # Try exact match first
        if ($policyPaths.ContainsKey($authType)) {
            $path = $policyPaths[$authType]
        } else {
            # Try case-insensitive match
            foreach ($key in $policyPaths.Keys) {
                if ($key -ieq $authType) {
                    $path = $policyPaths[$key]
                    break
                }
            }
        }
        if (-not $path) {
            $path = "Path not defined"
        }
        
        Write-Host "[OBJECT TYPE: $($authType.ToUpper())]" -ForegroundColor Yellow
        Write-Host "Location: $path" -ForegroundColor DarkGray
        
        # Validate calculation
        if ($defined.Count -ne ($boundCount + $unboundCount)) {
            Write-Host "ERROR: Calculation mismatch! Total != Bound + Unbound" -ForegroundColor Red
        }
        
        Write-Host "Statistics: Total=$($defined.Count) | " -NoNewline
        Write-Host "Bound=$boundCount " -ForegroundColor Green -NoNewline
        Write-Host "| " -NoNewline
        Write-Host "Unbound=$unboundCount" -ForegroundColor Red
        Write-Host ""
        
        # Sort objects alphabetically
        foreach ($objName in ($defined.Keys | Sort-Object)) {
            if ($bound.ContainsKey($objName)) {
                # Object is bound (used) - show in GREEN
                # Use different color for Actions
                if ($authType -match "Action$") {
                    Write-Host "  [✓] $objName" -ForegroundColor Cyan
                } else {
                    Write-Host "  [✓] $objName" -ForegroundColor Green
                }
                # Show binding details in GRAY
                foreach ($binding in $defined[$objName].BoundTo) {
                    if ($binding -match "GLOBAL_SYSTEM") {
                        Write-Host "      └─ $binding" -ForegroundColor Cyan
                    } elseif ($binding -match "policy not bound") {
                        Write-Host "      └─ $binding" -ForegroundColor DarkYellow
                    } else {
                        Write-Host "      └─ $binding" -ForegroundColor Gray
                    }
                }
            } else {
                # Object is unbound (unused) - show in RED
                $message = "  [X] $objName"
                
                # For login schemas, check if they're in a label but unused
                if ($authType -eq "authentication loginschema") {
                    # Check if schema is in a label
                    $labelPattern = "(?is)add authentication policylabel\s+(`"[^`"]+`"|\S+).*?-loginschema\s+$([regex]::Escape($objName))"
                    $labelMatches = [regex]::Matches(($ConfigLines -join "`n"), $labelPattern)
                    
                    if ($labelMatches.Count -gt 0) {
                        $labelName = $labelMatches[0].Groups[1].Value.Trim('"')
                        # Check if label has policies
                        $labelPolicyPattern = "(?is)bind authentication policylabel\s+$([regex]::Escape($labelName)).*?-policyName\s+(`"[^`"]+`"|\S+)"
                        $policyMatches = [regex]::Matches(($ConfigLines -join "`n"), $labelPolicyPattern)
                        
                        if ($policyMatches.Count -eq 0) {
                            $message += " (in label: $labelName but label has NO policies)"
                        } else {
                            $message += " (in label: $labelName but policies not bound to vserver)"
                        }
                    } else {
                        $message += " (not used anywhere)"
                    }
                } else {
                    # Other object types - use original messages
                    switch ($authType) {
                        "authentication vserver" { 
                            $message += " (not referenced in any profile or vserver)"
                        }
                        "authentication authnprofile" { 
                            $message += " (not bound to any vserver)"
                        }
                        "authentication policylabel" { 
                            $message += " (not used as nextFactor)"
                        }
                        "authentication policy" { 
                            $message += " (not bound anywhere)"
                        }
                        { $_ -in @("authentication ldapAction", "authentication OAuthAction", 
                                   "authentication samlAction", "authentication radiusAction", 
                                   "authentication epaAction") } {
                            $message += " (not used by any policy)"
                        }
                    }
                }
                Write-Host $message -ForegroundColor Red
            }
        }
        Write-Host ""
    }
}

# Summary
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "                              SUMMARY" -ForegroundColor Yellow
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host ""

Write-Host "Authentication Objects Analysis:" -ForegroundColor White
Write-Host "  |"
Write-Host "  |-- Total objects defined: $totalDefined"

# Validate global calculation
if ($totalDefined -eq ($totalBound + $totalUnbound)) {
    Write-Host "  |    |-- " -NoNewline
    Write-Host "Bound (in use): $totalBound" -ForegroundColor Green
    Write-Host "  |    |-- " -NoNewline
    Write-Host "Unbound (unused): $totalUnbound" -ForegroundColor $(if ($totalUnbound -gt 0) { "Red" } else { "Green" })
} else {
    Write-Host "  |    |-- ERROR: Calculation mismatch!" -ForegroundColor Red
    Write-Host "  |    |-- Bound: $totalBound, Unbound: $totalUnbound" -ForegroundColor Red
}

Write-Host "  |"
Write-Host "  |-- Object types analyzed: $($allPolicies.Count)" -ForegroundColor White

if ($totalUnbound -gt 0) {
    $percentage = [math]::Round(($totalUnbound / $totalDefined * 100), 1)
    Write-Host "`n[WARNING] $totalUnbound authentication object(s) ($percentage%) are not bound" -ForegroundColor Yellow
    Write-Host "Review these objects for potential cleanup" -ForegroundColor Yellow
} else {
    Write-Host "`n[EXCELLENT] All authentication objects are properly configured and in use!" -ForegroundColor Green
}

# Relationship diagram
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host "                    AUTHENTICATION FLOW DIAGRAM" -ForegroundColor Yellow
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host ""
Write-Host "Authentication Chain Flow (in analysis order):" -ForegroundColor White
Write-Host ""
Write-Host "How to read Action bindings:" -ForegroundColor DarkGray
Write-Host "  Action → Used by Policy → Policy bound to (vserver/label/global)" -ForegroundColor DarkGray
Write-Host "  Example: EUR_DC_LB → POLICY: LDAP_Admin_AUTH_POL (via GLOBAL_SYSTEM)" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Complete example from your config:" -ForegroundColor DarkGray
Write-Host "  EUR_DC_LB (ldapAction) → LDAP_Admin_AUTH_POL (policy) → System Global" -ForegroundColor DarkGray
Write-Host ""
Write-Host "Flow Diagram:" -ForegroundColor White
Write-Host ""
Write-Host "Login Schema Usage Paths:" -ForegroundColor DarkGray
Write-Host "  Path 1: Schema -> LoginSchemaPolicy -> vServer" -ForegroundColor DarkGray
Write-Host "  Path 2: Schema -> PolicyLabel -> Policy -> vServer" -ForegroundColor DarkGray
Write-Host "  Path 3: Schema -> PolicyLabel -> nextFactor on vServer" -ForegroundColor DarkGray
Write-Host ""
Write-Host "1. VPN/LB Vserver" -ForegroundColor Cyan
Write-Host "      |"
Write-Host "      └─> 2. Authentication Profile (authnProfile)" -ForegroundColor Yellow
Write-Host "              |"
Write-Host "              └─> 3. Authentication vServer (authnVsName)" -ForegroundColor Yellow
Write-Host "                      |"
Write-Host "                      ├─> 4. Login Schema" -ForegroundColor Green
Write-Host "                      |"
Write-Host "                      └─> 5. Authentication Policies" -ForegroundColor Green
Write-Host "                              |"
Write-Host "                              ├─> 6. Policy Labels (nextFactor)" -ForegroundColor Green
Write-Host "                              |      |"
Write-Host "                              |      └─> More Policies..." -ForegroundColor DarkGray
Write-Host "                              |"
Write-Host "                              └─> 7-11. ACTIONS (endpoints)" -ForegroundColor Magenta
Write-Host "                                     ├─> LDAP Actions" -ForegroundColor DarkMagenta
Write-Host "                                     ├─> OAUTH Actions" -ForegroundColor DarkMagenta
Write-Host "                                     ├─> SAML Actions" -ForegroundColor DarkMagenta
Write-Host "                                     ├─> RADIUS Actions" -ForegroundColor DarkMagenta
Write-Host "                                     └─> EPA Actions" -ForegroundColor DarkMagenta
Write-Host ""

Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "Analysis complete" -ForegroundColor Cyan