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
}