powershell max authent policies V2

# ============================================================================
# NetScaler Authentication Policy Analyzer - ENHANCED v2.4
# ============================================================================
# Analyse spécialisée pour les objets AAA - Application Traffic
#
# NOUVEAUTÉS v2.4:
# - Séparation des objets non utilisés en deux catégories claires
# - Section COMMANDES DE SUPPRESSION pour copier-coller facilement
# - Affichage des lignes de configuration comme preuve
# - Meilleure visibilité pour le nettoyage
#
# IMPORTANT: Chain of authentication:
# Actions (define HOW) -> Policies (define WHEN) -> Bindings (define WHERE)
# Example: LDAP_Action -> Auth_Policy -> bound to AAA_vServer
#
# === 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
# ============================================================================

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

# ============================================================================
# FONCTIONS PRINCIPALES
# ============================================================================

function Load-NSConf {
    param([string]$FilePath)
    
    if (Test-Path $FilePath) {
        Write-Host "`n[INFO] Chargement du fichier : $FilePath" -ForegroundColor Cyan
        return Get-Content -Path $FilePath -Encoding UTF8
    } else {
        Write-Host "`n[ERREUR] Le fichier $FilePath est introuvable !" -ForegroundColor Red
        exit 1
    }
}

# ADC Interface paths
$policyPaths = @{
    "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 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"
}

function Find-DefinedObjects {
    param(
        [string[]]$ConfigLines,
        [string]$ObjectType
    )
    
    $configString = $ConfigLines -join "`n"
    
    if ($ObjectType -match "Action$") {
        $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('"')
        # Capturer la ligne complète
        $linePattern = "(?im)^.*add.*$([regex]::Escape($objectName)).*$"
        $lineMatch = [regex]::Match($configString, $linePattern)
        
        $objects[$objectName] = @{
            Name = $objectName
            Type = $ObjectType
            BoundTo = @()
            BindingDetails = @()
            Line = if ($lineMatch.Success) { $lineMatch.Value } else { "" }
        }
    }
    
    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 = "(?im)^(bind authentication vserver\s+(\S+).*?-policy\s+(`"([^`"]+)`"|\S+).*)$"
            $matches = [regex]::Matches($configString, $directPattern)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $vserver = $match.Groups[2].Value.Trim('"')
                $policy = $match.Groups[3].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($policy)) {
                    if (-not $boundObjects.ContainsKey($policy)) {
                        $boundObjects[$policy] = @()
                    }
                    if ($fullLine -match "-priority\s+(\d+)") {
                        $priority = $Matches[1]
                        $boundObjects[$policy] += @{
                            'Description' = "AUTH_VSERVER: $vserver (priority: $priority)"
                            'Line' = $fullLine
                        }
                    } else {
                        $boundObjects[$policy] += @{
                            'Description' = "AUTH_VSERVER: $vserver"
                            'Line' = $fullLine
                        }
                    }
                }
            }
            
            # Bound to policy labels
            $labelPattern = "(?im)^(bind authentication policylabel\s+(\S+).*?-policyName\s+(`"([^`"]+)`"|\S+).*)$"
            $matches = [regex]::Matches($configString, $labelPattern)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $label = $match.Groups[2].Value.Trim('"')
                $policy = $match.Groups[3].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($policy)) {
                    if (-not $boundObjects.ContainsKey($policy)) {
                        $boundObjects[$policy] = @()
                    }
                    if ($fullLine -match "-priority\s+(\d+)") {
                        $priority = $Matches[1]
                        $boundObjects[$policy] += @{
                            'Description' = "POLICY_LABEL: $label (priority: $priority)"
                            'Line' = $fullLine
                        }
                    } else {
                        $boundObjects[$policy] += @{
                            'Description' = "POLICY_LABEL: $label"
                            'Line' = $fullLine
                        }
                    }
                }
            }
            
            # Bound globally
            $globalPattern = "(?im)^(bind system global\s+(`"([^`"]+)`"|\S+).*)$"
            $matches = [regex]::Matches($configString, $globalPattern)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $policy = $match.Groups[2].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($policy)) {
                    if (-not $boundObjects.ContainsKey($policy)) {
                        $boundObjects[$policy] = @()
                    }
                    $description = "GLOBAL_SYSTEM"
                    if ($fullLine -match "-priority\s+(\d+)") {
                        $priority = $Matches[1]
                        $description += " (priority: $priority)"
                    }
                    $boundObjects[$policy] += @{
                        'Description' = $description
                        'Line' = $fullLine
                    }
                }
            }
        }
        
        "authentication policylabel" {
            # Policy labels are bound as nextFactor
            $pattern = "(?im)^(bind authentication vserver\s+(\S+).*?-nextFactor\s+(\S+).*)$"
            $matches = [regex]::Matches($configString, $pattern)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $vserver = $match.Groups[2].Value.Trim('"')
                $label = $match.Groups[3].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($label)) {
                    if (-not $boundObjects.ContainsKey($label)) {
                        $boundObjects[$label] = @()
                    }
                    $boundObjects[$label] += @{
                        'Description' = "AUTH_VSERVER: $vserver (as nextFactor)"
                        'Line' = $fullLine
                    }
                }
            }
        }
        
        "authentication authnprofile" {
            # Profiles bound to VPN vservers
            $pattern = "(?im)^(add vpn vserver\s+(\S+).*?-authnProfile\s+(`"([^`"]+)`"|\S+).*)$"
            $matches = [regex]::Matches($configString, $pattern)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $vserver = $match.Groups[2].Value.Trim('"')
                $profile = $match.Groups[3].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($profile)) {
                    if (-not $boundObjects.ContainsKey($profile)) {
                        $boundObjects[$profile] = @()
                    }
                    $boundObjects[$profile] += @{
                        'Description' = "VPN_VSERVER: $vserver"
                        'Line' = $fullLine
                    }
                }
            }
            
            # Profiles bound to LB vservers
            $lbPattern = "(?im)^(add lb vserver\s+(\S+).*?-authnProfile\s+(`"([^`"]+)`"|\S+).*)$"
            $matches = [regex]::Matches($configString, $lbPattern)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $vserver = $match.Groups[2].Value.Trim('"')
                $profile = $match.Groups[3].Value.Trim('"')
                if ($DefinedObjects.ContainsKey($profile)) {
                    if (-not $boundObjects.ContainsKey($profile)) {
                        $boundObjects[$profile] = @()
                    }
                    $boundObjects[$profile] += @{
                        'Description' = "LB_VSERVER: $vserver"
                        'Line' = $fullLine
                    }
                }
            }
        }
        
        "authentication vserver" {
            # AAA vservers 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 = "(?im)^(add vpn vserver\s+(\S+).*?-authnProfile\s+(`"([^`"]+)`"|\S+).*)$"
            $matches = [regex]::Matches($configString, $patternBound)
            foreach ($match in $matches) {
                $fullLine = $match.Groups[1].Value
                $vserver = $match.Groups[2].Value.Trim('"')
                $profile = $match.Groups[3].Value.Trim('"')
                if ($mapping.ContainsKey($profile)) {
                    $aaaVserver = $mapping[$profile]
                    if ($DefinedObjects.ContainsKey($aaaVserver)) {
                        if (-not $boundObjects.ContainsKey($aaaVserver)) {
                            $boundObjects[$aaaVserver] = @()
                        }
                        $boundObjects[$aaaVserver] += @{
                            'Description' = "VPN_VSERVER: $vserver (via profile: $profile)"
                            'Line' = $fullLine
                        }
                    }
                }
            }
        }
        
        "authentication loginschema" {
            # Method 1: loginschema policies
            $policyPattern = "(?i)add authentication loginschemapolicy\s+(`"[^`"]+`"|\S+).*?-action\s+(`"[^`"]+`"|\S+)"
            $policyMatches = [regex]::Matches($configString, $policyPattern)
            
            $schemaPolicies = @{}
            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
            }
            
            foreach ($schemaName in $schemaPolicies.Keys) {
                if ($DefinedObjects.ContainsKey($schemaName)) {
                    foreach ($policyName in $schemaPolicies[$schemaName]) {
                        $bindPattern = "(?im)^(bind authentication vserver\s+(\S+).*?-policy\s+$([regex]::Escape($policyName)).*)$"
                        $bindMatches = [regex]::Matches($configString, $bindPattern)
                        foreach ($bindMatch in $bindMatches) {
                            $fullLine = $bindMatch.Groups[1].Value
                            $vserver = $bindMatch.Groups[2].Value.Trim('"')
                            if (-not $boundObjects.ContainsKey($schemaName)) {
                                $boundObjects[$schemaName] = @()
                            }
                            $boundObjects[$schemaName] += @{
                                'Description' = "AUTH_VSERVER: $vserver (via policy: $policyName)"
                                'Line' = $fullLine
                            }
                        }
                    }
                }
            }
            
            # Method 2: schemas in policy labels (only if label is actually used)
            $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 label has policies bound
                    $labelPolicyPattern = "(?is)bind authentication policylabel\s+$([regex]::Escape($labelName)).*?-policyName\s+(`"[^`"]+`"|\S+)"
                    $labelPolicyMatches = [regex]::Matches($configString, $labelPolicyPattern)
                    
                    $policiesInLabel = @()
                    foreach ($policyMatch in $labelPolicyMatches) {
                        $policyInLabel = $policyMatch.Groups[1].Value.Trim('"')
                        $policiesInLabel += $policyInLabel
                    }
                    
                    if ($policiesInLabel.Count -gt 0) {
                        # Check if policies are bound to vservers
                        foreach ($policy in $policiesInLabel) {
                            $vserverBindPattern = "(?im)^(bind authentication vserver\s+(\S+).*?-policy\s+$([regex]::Escape($policy)).*)$"
                            $vserverMatches = [regex]::Matches($configString, $vserverBindPattern)
                            
                            foreach ($vMatch in $vserverMatches) {
                                $fullLine = $vMatch.Groups[1].Value
                                $vserver = $vMatch.Groups[2].Value.Trim('"')
                                if (-not $boundObjects.ContainsKey($schemaName)) {
                                    $boundObjects[$schemaName] = @()
                                }
                                $boundObjects[$schemaName] += @{
                                    'Description' = "POLICY_LABEL: $labelName -> POLICY: $policy -> VSERVER: $vserver"
                                    'Line' = $fullLine
                                }
                            }
                        }
                    }
                }
            }
            
            # Method 3: nextFactor usage
            foreach ($match in $labelMatches) {
                $labelName = $match.Groups[1].Value.Trim('"')
                $schemaName = $match.Groups[2].Value.Trim('"')
                
                if ($DefinedObjects.ContainsKey($schemaName)) {
                    $nextFactorPattern = "(?im)^(bind authentication (?:vserver|policylabel)\s+(\S+).*?-nextFactor\s+$([regex]::Escape($labelName)).*)$"
                    $nextFactorMatches = [regex]::Matches($configString, $nextFactorPattern)
                    
                    foreach ($nfMatch in $nextFactorMatches) {
                        $fullLine = $nfMatch.Groups[1].Value
                        $vserver = $nfMatch.Groups[2].Value.Trim('"')
                        if (-not $boundObjects.ContainsKey($schemaName)) {
                            $boundObjects[$schemaName] = @()
                        }
                        $binding = @{
                            'Description' = "POLICY_LABEL: $labelName -> VSERVER: $vserver (as nextFactor)"
                            'Line' = $fullLine
                        }
                        # Éviter les doublons
                        $exists = $false
                        foreach ($existing in $boundObjects[$schemaName]) {
                            if ($existing['Description'] -eq $binding['Description']) {
                                $exists = $true
                                break
                            }
                        }
                        if (-not $exists) {
                            $boundObjects[$schemaName] += $binding
                        }
                    }
                }
            }
        }
        
        { $_ -in @("authentication ldapAction", "authentication OAuthAction", 
                   "authentication samlAction", "authentication radiusAction", 
                   "authentication epaAction") } {
            # Actions referenced in policies
            $policyPattern = "(?i)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('"')
                
                if ($DefinedObjects.ContainsKey($actionName)) {
                    # Get the line for this policy definition
                    $policyLinePattern = "(?im)^(add authentication policy\s+$([regex]::Escape($policyName)).*?)$"
                    $policyLineMatch = [regex]::Match($configString, $policyLinePattern)
                    $policyLine = if ($policyLineMatch.Success) { $policyLineMatch.Groups[1].Value } else { "" }
                    
                    if (-not $boundObjects.ContainsKey($actionName)) {
                        $boundObjects[$actionName] = @()
                    }
                    
                    # Check if policy is bound
                    $policyBound = $false
                    $bindingDetails = @()
                    
                    # Check vserver bindings
                    $vserverPattern = "(?im)^(bind authentication vserver\s+(\S+).*?-policy\s+$([regex]::Escape($policyName)).*)$"
                    $vserverMatches = [regex]::Matches($configString, $vserverPattern)
                    foreach ($vMatch in $vserverMatches) {
                        $fullLine = $vMatch.Groups[1].Value
                        $vserver = $vMatch.Groups[2].Value.Trim('"')
                        $bindingDetails += @{
                            'Type' = "vserver"
                            'Description' = "via AUTH_VSERVER: $vserver"
                            'Line' = $fullLine
                        }
                        $policyBound = $true
                    }
                    
                    # Check label bindings
                    $labelPattern = "(?im)^(bind authentication policylabel\s+(\S+).*?-policyName\s+$([regex]::Escape($policyName)).*)$"
                    $labelMatches = [regex]::Matches($configString, $labelPattern)
                    foreach ($lMatch in $labelMatches) {
                        $fullLine = $lMatch.Groups[1].Value
                        $label = $lMatch.Groups[2].Value.Trim('"')
                        $bindingDetails += @{
                            'Type' = "label"
                            'Description' = "via POLICY_LABEL: $label"
                            'Line' = $fullLine
                        }
                        $policyBound = $true
                    }
                    
                    # Check global bindings
                    $globalPattern = "(?im)^(bind system global\s+$([regex]::Escape($policyName)).*)$"
                    if ([regex]::IsMatch($configString, $globalPattern)) {
                        $globalMatch = [regex]::Match($configString, $globalPattern)
                        $fullLine = $globalMatch.Groups[1].Value
                        $bindingDetails += @{
                            'Type' = "global"
                            'Description' = "via GLOBAL_SYSTEM"
                            'Line' = $fullLine
                        }
                        $policyBound = $true
                    }
                    
                    # Add to bound objects
                    if ($policyBound) {
                        foreach ($detail in $bindingDetails) {
                            $boundObjects[$actionName] += @{
                                'Description' = "POLICY: $policyName ($($detail['Description']))"
                                'Line' = $detail['Line']
                                'PolicyLine' = $policyLine
                            }
                        }
                    }
                }
            }
        }
    }
    
    # Update defined objects
    foreach ($objName in $boundObjects.Keys) {
        if ($DefinedObjects.ContainsKey($objName)) {
            $DefinedObjects[$objName].BoundTo = $boundObjects[$objName]
        }
    }
    
    return $boundObjects
}

# ============================================================================
# AFFICHAGE DES RESULTATS
# ============================================================================

function Show-ObjectsWithSeparation {
    param(
        [hashtable]$DefinedObjects,
        [hashtable]$BoundObjects,
        [string]$ObjectType,
        [string]$ConfigString
    )
    
    # Séparer en catégories
    $orphanObjects = @()
    $referencedButUnboundObjects = @{}
    $boundObjectsList = @()
    
    foreach ($objName in ($DefinedObjects.Keys | Sort-Object)) {
        if ($BoundObjects.ContainsKey($objName) -and $BoundObjects[$objName].Count -gt 0) {
            # Objet bindé/utilisé
            $boundObjectsList += $objName
        } else {
            # Objet non bindé - déterminer pourquoi
            $isReferenced = $false
            $references = @()
            
            # Logique spécifique selon le type
            switch ($ObjectType.ToLower()) {
                "authentication loginschema" {
                    # Check si dans un label
                    $labelPattern = "(?is)add authentication policylabel\s+(`"[^`"]+`"|\S+).*?-login[Ss]chema\s+$([regex]::Escape($objName))"
                    $labelMatches = [regex]::Matches($ConfigString, $labelPattern)
                    
                    if ($labelMatches.Count -gt 0) {
                        foreach ($match in $labelMatches) {
                            $labelName = $match.Groups[1].Value.Trim('"')
                            $references += "Label: $labelName"
                            $isReferenced = $true
                        }
                    }
                }
                
                { $_ -in @("authentication ldapaction", "authentication oauthaction", 
                           "authentication samlaction", "authentication radiusaction", 
                           "authentication epaaction") } {
                    # Check si utilisée par une policy
                    $policyPattern = "(?i)add authentication policy\s+(`"[^`"]+`"|\S+).*?-action\s+$([regex]::Escape($objName))"
                    $policyMatches = [regex]::Matches($ConfigString, $policyPattern)
                    
                    if ($policyMatches.Count -gt 0) {
                        foreach ($match in $policyMatches) {
                            $policyName = $match.Groups[1].Value.Trim('"')
                            $references += "Policy: $policyName (not bound)"
                            $isReferenced = $true
                        }
                    }
                }
            }
            
            if ($isReferenced) {
                $referencedButUnboundObjects[$objName] = $references
            } else {
                $orphanObjects += $objName
            }
        }
    }
    
    # Affichage
    $totalUnused = $orphanObjects.Count + $referencedButUnboundObjects.Count
    
    if ($totalUnused -gt 0) {
        Write-Host "`n[OBJETS NON UTILISES: $totalUnused]" -ForegroundColor Red
        
        # [1] Objets orphelins
        if ($orphanObjects.Count -gt 0) {
            Write-Host "`n  [1] Orphelins (aucune reference) : $($orphanObjects.Count)" -ForegroundColor Yellow
            foreach ($objName in $orphanObjects) {
                Write-Host "    ├─ " -NoNewline -ForegroundColor Red
                Write-Host "$objName" -ForegroundColor Red
                Write-Host "    │  " -NoNewline -ForegroundColor DarkGray
                Write-Host "$($DefinedObjects[$objName].Line)" -ForegroundColor White
            }
        }
        
        # [2] Objets référencés mais non bindés
        if ($referencedButUnboundObjects.Count -gt 0) {
            Write-Host "`n  [2] References mais non bindes : $($referencedButUnboundObjects.Count)" -ForegroundColor Yellow
            foreach ($objName in ($referencedButUnboundObjects.Keys | Sort-Object)) {
                Write-Host "    ├─ " -NoNewline -ForegroundColor Red
                Write-Host "$objName" -ForegroundColor Red
                Write-Host "    │  " -NoNewline -ForegroundColor DarkGray
                Write-Host "$($DefinedObjects[$objName].Line)" -ForegroundColor White
                Write-Host "    │  " -NoNewline -ForegroundColor DarkGray
                Write-Host "Reference par :" -ForegroundColor DarkGray
                foreach ($ref in $referencedButUnboundObjects[$objName]) {
                    Write-Host "    │    • " -NoNewline -ForegroundColor DarkGray
                    Write-Host "$ref" -ForegroundColor Yellow
                }
            }
        }
    }
    
    # Objets utilisés
    if ($boundObjectsList.Count -gt 0) {
        Write-Host "`n[OBJETS UTILISES: $($boundObjectsList.Count)]" -ForegroundColor Green
        foreach ($objName in $boundObjectsList) {
            if ($ObjectType -match "Action$") {
                Write-Host "  [✓] $objName" -ForegroundColor Cyan
            } else {
                Write-Host "  [✓] $objName" -ForegroundColor Green
            }
            
            foreach ($binding in $BoundObjects[$objName]) {
                if ($binding['Description'] -match "GLOBAL_SYSTEM") {
                    Write-Host "      └─ $($binding['Description'])" -ForegroundColor Cyan
                } else {
                    Write-Host "      └─ $($binding['Description'])" -ForegroundColor Gray
                }
                Write-Host "         $($binding['Line'])" -ForegroundColor White
            }
        }
    }
}

# ============================================================================
# EXECUTION PRINCIPALE
# ============================================================================

Clear-Host

Write-Host "`n" -NoNewline
Write-Host "╔" -NoNewline -ForegroundColor Cyan
Write-Host "═══════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║" -NoNewline -ForegroundColor Cyan
Write-Host "                                                                               " -NoNewline
Write-Host "║" -ForegroundColor Cyan
Write-Host "║" -NoNewline -ForegroundColor Cyan
Write-Host "      NetScaler Authentication (AAA) Analysis - ENHANCED v2.4                  " -NoNewline -ForegroundColor Yellow
Write-Host "║" -ForegroundColor Cyan
Write-Host "║" -NoNewline -ForegroundColor Cyan
Write-Host "                                                                               " -NoNewline
Write-Host "║" -ForegroundColor Cyan
Write-Host "╚" -NoNewline -ForegroundColor Cyan
Write-Host "═══════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan

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

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

# Types d'objets à analyser (ordre logique)
$authTypes = @(
    "authentication authnprofile",
    "authentication vserver",
    "authentication loginschema",
    "authentication policy",
    "authentication policylabel",
    "authentication ldapAction",
    "authentication OAuthAction", 
    "authentication samlAction",
    "authentication radiusAction",
    "authentication epaAction"
)

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
    
    if ($defined.Count -gt 0) {
        $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"

# Afficher le RESUME
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host "                              RESUME" -ForegroundColor Yellow
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host ""

Write-Host "Authentication Objects Analysis:" -ForegroundColor White
Write-Host "  |"
Write-Host "  |-- Total objects defined: $totalDefined"
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" })
Write-Host "  |"
Write-Host "  |-- Object types analyzed: $($allPolicies.Count)" -ForegroundColor White

Write-Host "`n==================================================================" -ForegroundColor Cyan

# Générer les commandes de suppression
$allUnusedObjects = @()
foreach ($authType in $authTypes) {
    if ($allPolicies.ContainsKey($authType)) {
        $policies = $allPolicies[$authType]
        $defined = $policies.Defined
        $bound = $policies.Bound
        
        foreach ($objName in ($defined.Keys | Sort-Object)) {
            if (-not $bound.ContainsKey($objName) -or $bound[$objName].Count -eq 0) {
                $allUnusedObjects += @{
                    'Type' = $authType
                    'Name' = $objName
                }
            }
        }
    }
}

if ($allUnusedObjects.Count -gt 0) {
    Write-Host "`n==================================================================" -ForegroundColor Yellow
    Write-Host " COMMANDES DE SUPPRESSION (copier-coller)" -ForegroundColor Yellow -BackgroundColor Black
    Write-Host "==================================================================" -ForegroundColor Yellow
    Write-Host ""
    
    foreach ($obj in $allUnusedObjects) {
        $type = $obj['Type']
        $name = $obj['Name']
        
        # Générer la bonne commande selon le type
        if ($type -match "Action$") {
            $actualType = $type -replace "authentication ", ""
            Write-Host "rm authentication $actualType $name" -ForegroundColor White
        } else {
            Write-Host "rm $type $name" -ForegroundColor White
        }
    }
    
    Write-Host "`n==================================================================" -ForegroundColor Yellow
}

# Afficher les détails par type
Write-Host "`n" + ("=" * 80) -ForegroundColor Cyan
Write-Host "            AUTHENTICATION OBJECTS - DETAILED ANALYSIS" -ForegroundColor Yellow
Write-Host ("=" * 80) -ForegroundColor Cyan
Write-Host ""

$configString = $configLines -join "`n"

foreach ($authType in $authTypes) {
    if ($allPolicies.ContainsKey($authType)) {
        $policies = $allPolicies[$authType]
        $defined = $policies.Defined
        $bound = $policies.Bound
        $boundCount = $policies.BoundCount
        $unboundCount = $policies.UnboundCount
        
        $path = $policyPaths[$authType]
        if (-not $path) { $path = "Path not defined" }
        
        Write-Host "`n[OBJECT TYPE: $($authType.ToUpper())]" -ForegroundColor Yellow
        Write-Host "Location: $path" -ForegroundColor DarkGray
        Write-Host "Statistics: Total=$($defined.Count) | " -NoNewline
        Write-Host "Bound=$boundCount " -ForegroundColor Green -NoNewline
        Write-Host "| " -NoNewline
        Write-Host "Unbound=$unboundCount" -ForegroundColor Red
        
        Show-ObjectsWithSeparation -DefinedObjects $defined -BoundObjects $bound -ObjectType $authType -ConfigString $configString
    }
}

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