Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

Connexion d’une machine Linux à Active Directory avec SSSD

Intégrer des serveurs ou des postes Linux dans un environnement Windows Active Directory (AD) est un excellent moyen de centraliser la gestion des utilisateurs et des accès. Finie l’époque où il fallait jongler avec des configurations LDAP et Kerberos complexes et fragiles. Aujourd’hui, SSSD (System Security Services Daemon) fait le travail de manière robuste et sécurisée.

Voici un guide rapide pour lier votre machine Linux à votre domaine AD en quelques minutes.

Pourquoi choisir SSSD ?

  • Gestion du cache : Permet aux utilisateurs de se connecter même si le contrôleur de domaine est temporairement inaccessible.
  • Performance : Centralise les requêtes d’authentification et réduit la charge sur le réseau.
  • Simplicité : S’intègre parfaitement avec realm, un outil qui automatise la majeure partie de la configuration.

Étape 1 : Les prérequis indispensables

Avant de lancer les commandes, assurez-vous que :

  1. Votre machine Linux a une adresse IP fixe ou statique via DHCP.
  2. Le fichier /etc/resolv.conf pointe bien vers les serveurs DNS de votre Active Directory.
  3. L’heure de votre machine Linux est parfaitement synchronisée avec celle du contrôleur de domaine (le protocole Kerberos ne tolère pas plus de 5 minutes de décalage).

Étape 2 : Installation des paquets

Sur une distribution basée sur Ubuntu / Debian, installez les outils nécessaires avec la commande suivante :

Bash

sudo apt update
sudo apt install sssd sssd-tools realmd adcli krb5-user packagekit sssd-ad

(Note : Durant l’installation, Kerberos peut vous demander le nom de votre royaume. Inscrivez-le en MAJUSCULES, ex: MONENTREPRISE.LOCAL).

Étape 3 : Découverte et jonction au domaine

C’est ici que la magie de realmd opère. Tout d’abord, vérifiez que Linux voit bien votre domaine AD :

Bash

realm discover monentreprise.local

Si tout est correct, lancez la jonction au domaine. Vous aurez besoin des identifiants d’un compte Administrateur de l’AD (ou d’un compte ayant les droits de joindre des machines au domaine) :

Bash

sudo realm join -U Administrateur monentreprise.local

Entrez le mot de passe lorsque demandé. Si la commande s’exécute sans erreur, félicitations : votre machine fait officiellement partie du domaine !

Étape 4 : Utilisation et vérification

Pour vérifier que SSSD communique bien avec votre Active Directory, vous pouvez tester la résolution d’un utilisateur de l’AD avec la commande id :

Bash

id utilisateur_ad@monentreprise.local

Le terminal devrait vous renvoyer l’UID, le GID et les groupes Windows de l’utilisateur.

Astuce : Se connecter sans taper le nom de domaine

Par défaut, SSSD vous oblige à vous connecter sous la forme username@domain. Si vous préférez utiliser uniquement le nom d’utilisateur simple (username), modifiez le fichier /etc/sssd/sssd.conf et passez cette option à False :

Ini, TOML

use_fully_qualified_names = False

N’oubliez pas de redémarrer le service après modification :

Bash

sudo systemctl restart sssd

En conclusion

En utilisant le combo realmd + SSSD, l’intégration d’une machine Linux dans un Active Directory Windows devient un jeu d’enfant. Vous bénéficiez d’une authentification centralisée, sécurisée et d’une gestion simplifiée pour vos administrateurs système.

Secure Boot Certificates Power BI Dashboard : guide de déploiement avec Configuration Manager

Suite à l’expiration des certificats Secure Boot en juin, la visibilité sur l’état de conformité des PC est devenue un enjeu majeur.

Afin d’accompagner les équipes d’administration, nous vous partageons un tableau de bord Power BI dédié à Configuration Manager. Compatible avec les environnements Standalone et Co-Managed, il permet d’identifier rapidement les postes concernés, d’analyser les écarts de conformité et de faciliter le suivi des actions de remédiation.
Cet article présente les étapes de déploiement ainsi que les principales fonctionnalités de cette solution :

Prérequis – Tableau de bord Power BI Secure Boot Certificates

Téléchargement de la Configuration Baseline et du rapport Power BI.

Avant de commencer, téléchargez les éléments nécessaires :

Importation de la baseline de configuration Secure Boot

Par défaut, plusieurs informations nécessaires au suivi des certificats Secure Boot ne sont pas disponibles via Hardware Inventory de Configuration Manager.

L’utilisation d’une Configuration Baseline avec un état Compliant / Non-compliant constitue une méthode simple et efficace pour collecter ces informations et suivre la conformité des postes.
Étape 1 – Extraire les fichiers de la baseline Secure Boot Certificates

Étape 2 – Dans la console Configuration Manager, accédez à :
Actifs et conformité > Paramètres de conformité > Baselines de configuration, puis cliquez sur Importer des données de configuration.

Étape 3 – Cliquez sur Ajouter, puis sélectionnez tous les fichiers .cab extraits précédemment.
Étape 4 – Un avertissement relatif à un éditeur inconnu apparaît. Cliquez sur OK pour continuer.

Étape 5 – Cliquez sur Next afin de terminer l’assistant d’importation.

Étape 6 – Une fois créée, déployez la baseline de configuration Secure Boot Certificates.

Une fois créée, déployez la Secure Boot Certificates Configuration Baseline sur les collections de machines concernées.
Après un délai de 24 à 48 heures, vérifiez le nombre de machines Compliant / Non-compliant depuis la console Configuration Manager.

Secure Boot Certificates Power BI Dashboard

À présent, ouvrez le rapport Power BI téléchargé depuis Power BI Desktop afin de connecter les données et d’exploiter le tableau de bord Secure Boot Certificates.

Accept:

Indiquez le serveur SQL Configuration Manager et la base de données SQL utilisée:

Une demande d’autorisation pour la requête Native Database Query peut s’afficher. Cliquez sur OK pour continuer:

Une fois les requêtes terminées :

Script Powershell pour auditer les comptes desactivés, expirés ou proche de l’expiration

🔐 Mots de passe expirant prochainement : Anticipez les blocages des utilisateurs en identifiant qui doit changer son mot de passe sous $N$ jours.

⏱️ Comptes expirés : Repérez les comptes (souvent temporaires ou de prestataires) dont la date de fin de validité est dépassée.

🔒 Comptes désactivés : Listez les comptes inactifs afin de planifier leur suppression définitive.

Zéro bruit (Exclusions intelligentes) : Le script intègre un système de filtres automatiques pour ignorer les comptes de service (krbtgt, préfixes spécifiques SERV_, admapp_, etc.) et se concentrer uniquement sur les vrais comptes utilisateurs.

Rapport HTML : En plus de générer des fichiers CSV bruts pour chaque catégorie, le script compile les données dans un tableau HTML

Flexibilité : Vous pouvez ajuster les seuils de recherche directement via les paramètres (ex: chercher les mots de passe expirant dans 15 jours au lieu de 30).

#Requires -Modules ActiveDirectory
#Requires -Version 5.0

<#
.SYNOPSIS
    Génère un rapport consolidé des anomalies Active Directory
    
.DESCRIPTION
    Script qui regroupe tous les audits AD:
    - Utilisateurs avec mot de passe expirant prochainement
    - Comptes expirés depuis N jours
    - Comptes désactivés depuis N jours
    
    Génère des fichiers CSV et un rapport HTML consolid.

.PARAMETER DaysAheadPassword
    Nombre de jours à l'avance pour les mots de passe expirant.
    Défaut: 30

.PARAMETER DaysBackExpired
    Nombre de jours à remonter pour les comptes expirés.
    Défaut: 60

.PARAMETER DaysBackDisabled
    Nombre de jours à remonter pour les comptes désactivés.
    Défaut: 60

.PARAMETER OutputPath
    Répertoire de sortie pour les rapports.
    Défaut: C:\Temp\AD_Reports

.PARAMETER ExcludedObjectSid
    SID de l'objet à exclure (ex: BUILTIN\Invité).

.PARAMETER ExcludedPrefixes
    Tableau des préfixes de compte à exclure.

.EXAMPLE
    .\AD_Accounts_Disab_Expir_Audit.ps1
    
.EXAMPLE
    .\AD_Accounts_Disab_Expir_Audit.ps1 -DaysAheadPassword 15 -DaysBackExpired 90 -OutputPath "C:\Reports"
#>

param(
    [int]$DaysAheadPassword = 30,
    [int]$DaysBackExpired = 60,
    [int]$DaysBackDisabled = 60,
    [string]$OutputPath = "C:\Temp\AD_Reports",
    [string]$ExcludedObjectSid = "S-1-5-21-1801674531-261903793-725345543-501",
    [string[]]$ExcludedPrefixes = @("krbtgt", "TsInternetUser", "X_", "T_", "V_", "U_", "P_", "E_", "S_", "R_", "SERV_", "admapp_", "admpr_", "admex_")
)

# ============================================================================
# FUNCTIONS
# ============================================================================

function Write-Header {
    param([string]$Text)
    Write-Host ""
    Write-Host "════════════════════════════════════════════════════════════" -ForegroundColor Cyan
    Write-Host "  $Text" -ForegroundColor Cyan
    Write-Host "════════════════════════════════════════════════════════════" -ForegroundColor Cyan
}

function Write-Section {
    param([string]$Text)
    Write-Host "➤ $Text" -ForegroundColor Yellow
}

function Write-Success {
    param([string]$Text)
    Write-Host "  ✓ $Text" -ForegroundColor Green
}

function Write-Error-Custom {
    param([string]$Text)
    Write-Host "  ✗ $Text" -ForegroundColor Red
}

function Build-ExclusionFilter {
    <#
    .SYNOPSIS
        Construit le filtre LDAP pour exclure les comptes de service
    #>
    $filterParts = @()
    foreach ($prefix in $ExcludedPrefixes) {
        $filterParts += "(!samaccountname=$prefix*)"
    }
    return ($filterParts -join "")
}

function Build-SelectedProperties {
    <#
    .SYNOPSIS
        Construit les propriétés à afficher dans le rapport
    #>
    return @(
        @{Name="Nom Affiche";Expression={$_."DisplayName"}},
        @{Name="Nom";Expression={$_."sn"}},
        @{Name="Prenom";Expression={$_."GivenName"}},
        @{Name="Login";Expression={$_."SamAccountName"}},
        @{Name="Description";Expression={$_."description"}},
        @{Name="Adresse email";Expression={$_."Mail"}},
        @{Name="Direction/Entite";Expression={$_."Division"}},
        @{Name="Service";Expression={$_."department"}},
        @{Name="Fonction";Expression={$_."title"}},
        @{Name="Bureau";Expression={$_."PhysicalDeliveryOfficeName"}},
        @{Name="Telephone fixe";Expression={$_."TelephoneNumber"}},
        @{Name="Numero IP";Expression={$_."IPPhone"}},
        @{Name="Telephone mobile";Expression={$_."Mobile"}},
        @{Name="Code taxation societe";Expression={$_."extensionAttribute9"}},
        @{Name="Code taxation service";Expression={$_."extensionAttribute10"}},
        @{Name="Date modification";Expression={$_."whenChanged"}}
    )
}

function Get-AllADProperties {
    <#
    .SYNOPSIS
        Retourne la liste complète des propriétés à récupérer
    #>
    return @(
        'DisplayName', 'sn', 'GivenName', 'SamAccountName', 'description', 
        'Mail', 'Division', 'department', 'title', 'PhysicalDeliveryOfficeName',
        'TelephoneNumber', 'IPPhone', 'Mobile', 'extensionAttribute9', 
        'extensionAttribute10', 'whenChanged', 'msDS-UserPasswordExpiryTimeComputed',
        'PasswordNeverExpires', 'Enabled', 'accountExpires'
    )
}

function Get-PasswordExpiringUsers {
    <#
    .SYNOPSIS
        Récupère les utilisateurs avec mots de passe expirant dans N jours
    #>
    Write-Section "Recherche des mots de passe expirant dans $DaysAheadPassword jours..."
    
    try {
        $DateStart = Get-Date
        $DateEnd = $DateStart.AddDays($DaysAheadPassword)
        
        $properties = Get-AllADProperties
        $exclusionFilter = Build-ExclusionFilter
        
        # Filtre pour les mots de passe expirant
        $filter = "(&(objectCategory=person)(objectClass=user)(enabled=TRUE)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(PasswordNeverExpires=TRUE))(!(objectSid=$ExcludedObjectSid))$exclusionFilter)"
        
        $allUsers = Get-ADUser -Filter {Enabled -eq $True -and PasswordNeverExpires -eq $False} -Properties $properties
        
        $results = @()
        foreach ($user in $allUsers) {
            $pwdExpiry = $user."msDS-UserPasswordExpiryTimeComputed"
            if ($pwdExpiry) {
                try {
                    $expiryDate = [DateTime]::FromFileTime($pwdExpiry)
                    if ($expiryDate -gt $DateStart -and $expiryDate -le $DateEnd) {
                        $userobj = $user | Select-Object (Build-SelectedProperties)
                        $userobj | Add-Member -MemberType NoteProperty -Name "Date expiration MdP" -Value ($expiryDate.ToString('dd/MM/yyyy')) -Force
                        $results += $userobj
                    }
                }
                catch { }
            }
        }
        
        Write-Success "$($results.Count) utilisateur(s) avec mot de passe expirant"
        return $results | Sort-Object "Date expiration MdP"
    }
    catch {
        Write-Error-Custom "Erreur: $_"
        return @()
    }
}

function Get-ExpiredAccounts {
    <#
    .SYNOPSIS
        Récupère les comptes expirés depuis N jours
    #>
    Write-Section "Recherche des comptes expirés depuis $DaysBackExpired jours..."
    
    try {
        $Date = (Get-Date).AddDays(-$DaysBackExpired)
        $properties = Get-AllADProperties
        $exclusionFilter = Build-ExclusionFilter
        
        $filter = "(&(!useraccountcontrol:1.2.840.113556.1.4.803:=2)(objectCategory=person)(objectClass=user)(accountExpires<=$($Date.ToFileTime()))(!(|(accountExpires=9223372036854775807)(accountExpires=0)))(!objectSid=$ExcludedObjectSid)$exclusionFilter)"
        
        $results = Get-ADUser -LDAPFilter $filter -Properties $properties |
            Where-Object {
                $ae = $_.accountExpires
                $ae -gt 0 -and $ae -ne 9223372036854775807
            } |
            ForEach-Object {
                $user = $_
                $expiryDate = [DateTime]::FromFiletime([Int64]($user.accountExpires))
                $userobj = $user | Select-Object (Build-SelectedProperties)
                $userobj | Add-Member -MemberType NoteProperty -Name "Date expiration" -Value ($expiryDate.ToString('dd/MM/yyyy')) -Force
                $userobj
            } |
            Sort-Object "Date expiration"
        
        Write-Success "$($results.Count) compte(s) expiré(s)"
        return @($results)
    }
    catch {
        Write-Error-Custom "Erreur: $_"
        return @()
    }
}

function Get-DisabledAccounts {
    <#
    .SYNOPSIS
        Récupère les comptes désactivés depuis N jours
    #>
    Write-Section "Recherche des comptes désactivés depuis $DaysBackDisabled jours..."
    
    try {
        $Date = (Get-Date).AddDays(-$DaysBackDisabled)
        $properties = Get-AllADProperties
        $exclusionFilter = Build-ExclusionFilter
        
        $filterParts = @(
            "(&(useraccountcontrol:1.2.840.113556.1.4.803:=2)(objectclass=user)"
            "(!objectSid=$ExcludedObjectSid)"
        )
        
        $filter = $filterParts -join "" + "$exclusionFilter)"
        
        $results = Get-ADUser -LDAPFilter $filter -Properties $properties |
            Select-Object (Build-SelectedProperties) |
            Sort-Object "Nom Affiche"
        
        Write-Success "$($results.Count) compte(s) désactivé(s)"
        return @($results)
    }
    catch {
        Write-Error-Custom "Erreur: $_"
        return @()
    }
}

function Export-ToCSV {
    <#
    .SYNOPSIS
        Exporte les données en fichier CSV
    #>
    param(
        [array]$Data,
        [string]$FileName
    )
    
    $filePath = Join-Path $OutputPath $FileName
    
    if ($Data -and $Data.Count -gt 0) {
        $Data | Export-Csv -Encoding UTF8 -NoTypeInformation -Path $filePath -Force
        Write-Success "Exporté: $FileName"
    }
    else {
        "Aucune donnée à exporter." | Out-File -Encoding UTF8 -FilePath $filePath -Force
        Write-Host "  ⚠ Exporté (vide): $FileName" -ForegroundColor Gray
    }
    
    return $filePath
}

function Generate-HTMLReport {
    <#
    .SYNOPSIS
        Génère un rapport HTML consolidé
    #>
    param(
        [array]$PasswordExpiringUsers,
        [array]$ExpiredAccounts,
        [array]$DisabledAccounts
    )
    
    Write-Section "Génération du rapport HTML..."
    
    $timestamp = Get-Date -Format "dd/MM/yyyy HH:mm:ss"
    $htmlPath = Join-Path $OutputPath "AD_Consolidated_Report.html"
    
    $totalIssues = ($PasswordExpiringUsers.Count) + ($ExpiredAccounts.Count) + ($DisabledAccounts.Count)
    
    $htmlContent = @"
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Audit de l'expiration et desactivation des comptes Active Directory</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.3);
            overflow: hidden;
        }
        
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 40px 20px;
            text-align: center;
        }
        
        .header h1 {
            font-size: 32px;
            margin-bottom: 10px;
        }
        
        .header p {
            font-size: 14px;
            opacity: 0.9;
        }
        
        .content {
            padding: 30px 20px;
        }
        
        .summary-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
            margin-bottom: 40px;
        }
        
        .summary-card {
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            padding: 20px;
            border-radius: 8px;
            border-left: 4px solid #667eea;
            text-align: center;
        }
        
        .summary-card.warning {
            border-left-color: #ff6b6b;
            background: linear-gradient(135deg, #ffe5e5 0%, #ffcccc 100%);
        }
        
        .summary-card.danger {
            border-left-color: #ff4757;
            background: linear-gradient(135deg, #ffd6d6 0%, #ffb3b3 100%);
        }
        
        .summary-card.success {
            border-left-color: #2ed573;
            background: linear-gradient(135deg, #e5f9e5 0%, #ccf0cc 100%);
        }
        
        .summary-card h3 {
            font-size: 12px;
            text-transform: uppercase;
            color: #666;
            margin-bottom: 10px;
        }
        
        .summary-card .number {
            font-size: 28px;
            font-weight: bold;
            color: #333;
        }
        
        .section {
            margin-bottom: 40px;
        }
        
        .section h2 {
            border-bottom: 2px solid #667eea;
            padding-bottom: 10px;
            color: #333;
            margin-bottom: 20px;
            font-size: 20px;
        }
        
        table {
            width: 100%;
            border-collapse: collapse;
            background: white;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        
        th {
            background: #f8f9fa;
            color: #333;
            padding: 12px;
            text-align: left;
            font-weight: 600;
            border-bottom: 1px solid #ddd;
            font-size: 13px;
        }
        
        td {
            padding: 10px 12px;
            border-bottom: 1px solid #eee;
            font-size: 13px;
            color: #666;
        }
        
        tr:hover {
            background: #f8f9fa;
        }
        
        tr:last-child td {
            border-bottom: none;
        }
        
        .empty-message {
            text-align: center;
            padding: 30px;
            color: #999;
            background: #f8f9fa;
            border-radius: 8px;
        }
        
        .footer {
            background: #f8f9fa;
            padding: 20px;
            text-align: center;
            border-top: 1px solid #eee;
            color: #999;
            font-size: 12px;
        }
        
        .total-issues {
            font-size: 16px;
            font-weight: bold;
            color: #2c3e50;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📊 Audit de l'expiration et desactivation des comptes Active Directory</h1>
            <p>Généré le $timestamp</p>
        </div>
        
        <div class="content">
            <div class="total-issues">
                Nombre total de comptes détectés: <span style="color: #ff6b6b;">$totalIssues</span>
            </div>
            
            <div class="summary-grid">
                <div class="summary-card warning">
                    <h3>Mots de passe expirant</h3>
                    <div class="number">${($PasswordExpiringUsers.Count)}</div>
                </div>
                <div class="summary-card danger">
                    <h3>Comptes expirés</h3>
                    <div class="number">${($ExpiredAccounts.Count)}</div>
                </div>
                <div class="summary-card danger">
                    <h3>Comptes désactivés</h3>
                    <div class="number">${($DisabledAccounts.Count)}</div>
                </div>
            </div>
"@
    
    # Section Mots de passe expirant
    $htmlContent += "<div class='section'>"
    $htmlContent += "<h2>🔐 Mots de passe expirant prochainement (dans $DaysAheadPassword jours)</h2>"
    
    if ($PasswordExpiringUsers -and $PasswordExpiringUsers.Count -gt 0) {
        $htmlContent += "<table><thead><tr>"
        $htmlContent += "<th>Login</th><th>Nom Affiche</th><th>Email</th><th>Service</th><th>Date expiration MdP</th>"
        $htmlContent += "</tr></thead><tbody>"
        
        foreach ($user in $PasswordExpiringUsers) {
            $htmlContent += "<tr>"
            $htmlContent += "<td>$($user.'Login')</td>"
            $htmlContent += "<td>$($user.'Nom Affiche')</td>"
            $htmlContent += "<td>$($user.'Adresse email')</td>"
            $htmlContent += "<td>$($user.'Service')</td>"
            $htmlContent += "<td>$($user.'Date expiration MdP')</td>"
            $htmlContent += "</tr>"
        }
        
        $htmlContent += "</tbody></table>"
    }
    else {
        $htmlContent += "<div class='empty-message'>✓ Aucun mot de passe n'expire dans les prochains $DaysAheadPassword jours</div>"
    }
    
    $htmlContent += "</div>"
    
    # Section Comptes expirés
    $htmlContent += "<div class='section'>"
    $htmlContent += "<h2>⏱️ Comptes expirés (depuis $DaysBackExpired jours)</h2>"
    
    if ($ExpiredAccounts -and $ExpiredAccounts.Count -gt 0) {
        $htmlContent += "<table><thead><tr>"
        $htmlContent += "<th>Login</th><th>Nom Affiche</th><th>Email</th><th>Service</th><th>Date expiration</th>"
        $htmlContent += "</tr></thead><tbody>"
        
        foreach ($user in $ExpiredAccounts) {
            $htmlContent += "<tr>"
            $htmlContent += "<td>$($user.'Login')</td>"
            $htmlContent += "<td>$($user.'Nom Affiche')</td>"
            $htmlContent += "<td>$($user.'Adresse email')</td>"
            $htmlContent += "<td>$($user.'Service')</td>"
            $htmlContent += "<td>$($user.'Date expiration')</td>"
            $htmlContent += "</tr>"
        }
        
        $htmlContent += "</tbody></table>"
    }
    else {
        $htmlContent += "<div class='empty-message'>✓ Aucun compte expiré</div>"
    }
    
    $htmlContent += "</div>"
    
    # Section Comptes désactivés
    $htmlContent += "<div class='section'>"
    $htmlContent += "<h2>🔒 Comptes désactivés (depuis $DaysBackDisabled jours)</h2>"
    
    if ($DisabledAccounts -and $DisabledAccounts.Count -gt 0) {
        $htmlContent += "<table><thead><tr>"
        $htmlContent += "<th>Login</th><th>Nom Affiche</th><th>Email</th><th>Service</th><th>Date modification</th>"
        $htmlContent += "</tr></thead><tbody>"
        
        foreach ($user in $DisabledAccounts) {
            $htmlContent += "<tr>"
            $htmlContent += "<td>$($user.'Login')</td>"
            $htmlContent += "<td>$($user.'Nom Affiche')</td>"
            $htmlContent += "<td>$($user.'Adresse email')</td>"
            $htmlContent += "<td>$($user.'Service')</td>"
            $htmlContent += "<td>$($user.'Date modification')</td>"
            $htmlContent += "</tr>"
        }
        
        $htmlContent += "</tbody></table>"
    }
    else {
        $htmlContent += "<div class='empty-message'>✓ Aucun compte désactivé</div>"
    }
    
    $htmlContent += "</div>"
    
    $htmlContent += @"
        </div>
        
        <div class="footer">
            <p>Rapport généré automatiquement par AD_Accounts_Disab_Expir_Audit.ps1</p>
            <p>$timestamp</p>
        </div>
    </div>
</body>
</html>
"@
    
    $htmlContent | Out-File -Encoding UTF8 -FilePath $htmlPath -Force
    Write-Success "Rapport HTML généré: AD_Consolidated_Report.html"
    
    return $htmlPath
}

# ============================================================================
# MAIN
# ============================================================================

try {
    # Vérification du module ActiveDirectory
    Write-Header "Audit de l'expiration et desactivation des comptes Active Directory"
    
    if (!(Get-Module ActiveDirectory)) {
        Write-Section "Import du module ActiveDirectory..."
        Import-Module ActiveDirectory -ErrorAction Stop
        Write-Success "Module ActiveDirectory importé"
    }
    
    # Créer le répertoire de sortie s'il n'existe pas
    if (!(Test-Path $OutputPath)) {
        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
        Write-Success "Répertoire créé: $OutputPath"
    }
    else {
        Write-Success "Répertoire de sortie: $OutputPath"
    }
    
    Write-Header "Lancement des audits"
    
    # Récupérer les données
    $passwordExpiring = Get-PasswordExpiringUsers
    $expiredAccounts = Get-ExpiredAccounts
    $disabledAccounts = Get-DisabledAccounts
    
    Write-Header "Exportation des données"
    
    # Exporter en CSV
    Export-ToCSV -Data $passwordExpiring -FileName "PasswordExpiring-$($DaysAheadPassword)Days.csv"
    Export-ToCSV -Data $expiredAccounts -FileName "AccountExpired-$($DaysBackExpired)Days.csv"
    Export-ToCSV -Data $disabledAccounts -FileName "AccountDisabled-$($DaysBackDisabled)Days.csv"
    
    # Générer le rapport HTML
    Write-Header "Génération du rapport"
    $reportPath = Generate-HTMLReport -PasswordExpiringUsers $passwordExpiring -ExpiredAccounts $expiredAccounts -DisabledAccounts $disabledAccounts
    
    # Résumé final
    Write-Host ""
    Write-Host "╔════════════════════════════════════════════════════════════╗" -ForegroundColor Green
    Write-Host "║                   ✓ AUDIT TERMINÉ AVEC SUCCÈS              ║" -ForegroundColor Green
    Write-Host "╚════════════════════════════════════════════════════════════╝" -ForegroundColor Green
    Write-Host ""
    Write-Host "Résumé:" -ForegroundColor Cyan
    Write-Host "  • Mots de passe expirant: $($passwordExpiring.Count)" -ForegroundColor Yellow
    Write-Host "  • Comptes expirés: $($expiredAccounts.Count)" -ForegroundColor Red
    Write-Host "  • Comptes désactivés: $($disabledAccounts.Count)" -ForegroundColor Red
    Write-Host "  • Total comptes: $(($passwordExpiring.Count) + ($expiredAccounts.Count) + ($disabledAccounts.Count))" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Fichiers générés dans: $OutputPath" -ForegroundColor Green
    Write-Host ""
    
    # Proposer d'ouvrir le rapport
    $response = Read-Host "Voulez-vous ouvrir le rapport HTML? (O/N)"
    if ($response -eq "O" -or $response -eq "o") {
        Start-Process $reportPath
    }
}
catch {
    Write-Host ""
    Write-Host "╔════════════════════════════════════════════════════════════╗" -ForegroundColor Red
    Write-Host "║                    ✗ ERREUR FATALE                        ║" -ForegroundColor Red
    Write-Host "╚════════════════════════════════════════════════════════════╝" -ForegroundColor Red
    Write-Host ""
    Write-Error $_
    exit 1
}