faitza.com
faitza.com
> blog_tech

Revue périodique des comptes AD/Entra avec PowerShell

Calcul... #powershell #active-directory #entra-id #security #governance #iam

Introduction

La revue périodique des accès est une exigence de toute politique de gouvernance IAM (Identity & Access Management). En pratique, deux populations de comptes concentrent la majorité des risques : les comptes inactifs (utilisateurs qui ne se connectent plus mais dont le compte reste actif) et les comptes désactivés qui traînent dans l'AD avec leurs appartenances à des groupes.

Faitza_Revue.ps1 automatise la détection de ces deux populations, corrèle les données AD avec les logs de connexion Entra ID (Microsoft Graph), orchestre les notifications aux managers et produit des rapports Excel via le module Faitza_Export.

Prérequis

Le module ActiveDirectory et le module Microsoft.Graph doivent être installés. Le compte d'exécution nécessite les droits en lecture sur tous les contrôleurs de domaine (pour interroger lastLogon) et le scope Graph AuditLog.Read.All pour les logs de connexion Entra ID.

Comptes inactifs — Get-Faitza_Revue_comptes_inactifs

Get-Faitza_Revue_comptes_inactifs détecte les comptes utilisateurs actifs dont la dernière connexion est antérieure au seuil configuré (défaut : 90 jours). Le défi principal est que lastLogon n'est pas répliqué entre les DC — chaque contrôleur stocke sa propre valeur locale. Il faut interroger tous les DC et conserver le maximum.

function Get-Faitza_Revue_comptes_inactifs {
    <#
    .SYNOPSIS
        Trouve les utilisateurs AD actifs qui ne se sont pas connectés depuis N jours.
        Corrèle avec les logs de connexion Entra ID pour affiner la détection.
    .PARAMETER ThresholdDays
        Nombre de jours d'inactivité avant de considérer le compte comme inactif. Défaut : 90.
    .PARAMETER SearchBase
        OU racine de la recherche. Si omis, cherche dans tout le domaine.
    .PARAMETER CorrelateEntra
        Si $true, interroge Microsoft Graph pour compléter avec les sign-in logs Entra.
    .OUTPUTS
        PSCustomObject[] — UPN, LastLogon, LastEntraSignIn, Manager, OU, AccountEnabled.
    #>
    [CmdletBinding()]
    param(
        [int]$ThresholdDays   = 90,
        [string]$SearchBase,
        [switch]$CorrelateEntra,
        [switch]$show
    )

    begin {
        if ($show) { Write-Host "$($PSStyle.Background.Blue)>>> Get-Faitza_Revue_comptes_inactifs <<<$($PSStyle.Reset)" }
        $cutoff = (Get-Date).AddDays(-$ThresholdDays)
        Write-Verbose "Seuil d'inactivité : $($cutoff.ToString('yyyy-MM-dd')) ($ThresholdDays jours)"

        # Récupérer tous les DCs du domaine courant
        $allDCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
        Write-Verbose "Contrôleurs de domaine détectés : $($allDCs -join ', ')"
    }

    process {
        # ─── Étape 1 : Récupérer tous les comptes utilisateurs actifs ────────────────
        $adParams = @{
            Filter     = { Enabled -eq $true }
            Properties = @("lastLogon", "lastLogonTimestamp", "userPrincipalName",
                           "manager", "distinguishedName", "mail", "extensionAttribute1")
        }
        if ($SearchBase) { $adParams["SearchBase"] = $SearchBase }

        $users = Get-ADUser @adParams
        Write-Verbose "Utilisateurs actifs trouvés : $($users.Count)"

        # ─── Étape 2 : Corrélation multi-DC pour lastLogon ───────────────────────────
        # lastLogon = timestamp local (non répliqué) → prendre le MAX sur tous les DCs
        Write-Verbose "Démarrage de la corrélation multi-DC (peut prendre plusieurs minutes)..."

        $lastLogonByUser = @{}   # clé = SamAccountName, valeur = DateTime max

        foreach ($dc in $allDCs) {
            Write-Verbose "  → Interrogation du DC : $dc"
            try {
                $dcParams = @{
                    Filter     = { Enabled -eq $true }
                    Properties = "lastLogon"
                    Server     = $dc
                }
                if ($SearchBase) { $dcParams["SearchBase"] = $SearchBase }

                Get-ADUser @dcParams | ForEach-Object {
                    $sam = $_.SamAccountName
                    if ($_.lastLogon -and $_.lastLogon -gt 0) {
                        $dt = [DateTime]::FromFileTime($_.lastLogon)
                        if (-not $lastLogonByUser.ContainsKey($sam) -or $dt -gt $lastLogonByUser[$sam]) {
                            $lastLogonByUser[$sam] = $dt
                        }
                    }
                }
            }
            catch {
                Write-Warning "Impossible de joindre le DC '$dc' : $($_.Exception.Message)"
            }
        }

        # ─── Étape 3 : Corrélation Entra ID (optionnelle) ────────────────────────────
        $entraSignIns = @{}
        if ($CorrelateEntra) {
            Write-Verbose "Récupération des dernières connexions Entra ID via Microsoft Graph..."
            # Nécessite le scope AuditLog.Read.All
            # signInActivity est disponible sur l'objet User via beta endpoint
            $graphUsers = Get-MgBetaUser -All -Property "userPrincipalName,signInActivity" `
                -Filter "accountEnabled eq true" `
                -Select "userPrincipalName,signInActivity"

            foreach ($gu in $graphUsers) {
                if ($gu.SignInActivity.LastSignInDateTime) {
                    $entraSignIns[$gu.UserPrincipalName] = $gu.SignInActivity.LastSignInDateTime
                }
            }
            Write-Verbose "Sign-in Entra récupérés : $($entraSignIns.Count) comptes"
        }

        # ─── Étape 4 : Filtrer et construire le résultat ─────────────────────────────
        $inactive = foreach ($user in $users) {
            $sam     = $user.SamAccountName
            $upn     = $user.UserPrincipalName

            # La vraie dernière connexion = MAX(lastLogon multi-DC, lastLogonTimestamp)
            $lastLogonDC        = if ($lastLogonByUser.ContainsKey($sam)) { $lastLogonByUser[$sam] } else { $null }
            $lastLogonTimestamp = if ($user.lastLogonTimestamp -and $user.lastLogonTimestamp -gt 0) {
                                    [DateTime]::FromFileTime($user.lastLogonTimestamp)
                                  } else { $null }

            $allDates  = @($lastLogonDC, $lastLogonTimestamp) | Where-Object { $_ -ne $null }
            $lastLogon = if ($allDates) { ($allDates | Measure-Object -Maximum).Maximum } else { $null }

            # Corrélation Entra : prendre le plus récent entre AD et Entra
            $lastEntra = if ($entraSignIns.ContainsKey($upn)) { $entraSignIns[$upn] } else { $null }
            if ($lastEntra -and ($null -eq $lastLogon -or $lastEntra -gt $lastLogon)) {
                $lastLogon = $lastEntra
            }

            # Exclure si connexion récente
            if ($lastLogon -and $lastLogon -ge $cutoff) { continue }

            # Résoudre le manager
            $managerUpn = $null
            if ($user.Manager) {
                $mgr = Get-ADUser -Identity $user.Manager -Properties userPrincipalName -ErrorAction SilentlyContinue
                $managerUpn = $mgr.UserPrincipalName
            }

            # Extraire l'OU depuis le DN
            $ou = ($user.DistinguishedName -split ",", 2)[1]

            [PSCustomObject]@{
                UPN              = $upn
                SamAccountName   = $sam
                DisplayName      = $user.Name
                LastLogon        = $lastLogon
                LastEntraSignIn  = $lastEntra
                InactiveDays     = if ($lastLogon) { [int](New-TimeSpan -Start $lastLogon -End (Get-Date)).TotalDays } else { 9999 }
                Manager          = $managerUpn
                OU               = $ou
                AccountEnabled   = $user.Enabled
                Mail             = $user.mail
            }
        }

        return $inactive | Sort-Object InactiveDays -Descending
    }

    end {
        if ($show) {
            Write-Host "Comptes inactifs détectés : $(@($inactive).Count)" -ForegroundColor Yellow
            Write-Host "$($PSStyle.Background.Blue)<<< Get-Faitza_Revue_comptes_inactifs >>>$($PSStyle.Reset)"
        }
    }
}

# Appel : comptes inactifs depuis 90 jours, avec corrélation Entra
$inactifs = Get-Faitza_Revue_comptes_inactifs `
    -ThresholdDays  90 `
    -CorrelateEntra `
    -show

$inactifs | Select-Object UPN, LastLogon, InactiveDays, Manager | Format-Table -AutoSize
lastLogon vs lastLogonTimestamp

lastLogon est mis à jour à chaque connexion mais n'est pas répliqué entre DC. lastLogonTimestamp est répliqué, mais avec un délai configurable (par défaut jusqu'à 14 jours de décalage — paramètre msDS-LogonTimeSyncInterval). La corrélation multi-DC sur lastLogon donne toujours la valeur la plus précise.

Comptes désactivés — Get-Faitza_Revue_comptes_desactives

Get-Faitza_Revue_comptes_desactives liste les comptes AD qui sont désactivés mais pas encore supprimés. Ces comptes sont un risque de gouvernance : ils conservent souvent leurs appartenances à des groupes (et donc des droits potentiels), et leur présence dans l'annuaire peut induire en erreur lors des audits.

La convention de l'environnement est d'écrire la date de désactivation dans le champ Description de l'objet AD. La fonction parse ce champ pour calculer l'ancienneté de la désactivation.

function Get-Faitza_Revue_comptes_desactives {
    <#
    .SYNOPSIS
        Liste les comptes AD désactivés avec leur date de désactivation, OU et groupes restants.
    .PARAMETER SearchBase
        OU racine de la recherche. Si omis, cherche dans tout le domaine.
    .PARAMETER MinDaysDisabled
        Ne retourner que les comptes désactivés depuis au moins N jours. Défaut : 0 (tous).
    .OUTPUTS
        PSCustomObject[] — UPN, DateDesactivation, JoursDesactive, OU, Groupes[].
    #>
    [CmdletBinding()]
    param(
        [string]$SearchBase,
        [int]$MinDaysDisabled = 0,
        [switch]$show
    )

    begin {
        if ($show) { Write-Host "$($PSStyle.Background.Blue)>>> Get-Faitza_Revue_comptes_desactives <<<$($PSStyle.Reset)" }
        # Pattern de date dans le champ Description : "Désactivé le 2025-03-15 — Départ collaborateur"
        $datePattern = [regex]'\d{4}-\d{2}-\d{2}'
    }

    process {
        $adParams = @{
            Filter     = { Enabled -eq $false }
            Properties = @("userPrincipalName", "Description", "MemberOf",
                           "distinguishedName", "mail", "manager", "whenChanged")
        }
        if ($SearchBase) { $adParams["SearchBase"] = $SearchBase }

        $disabledUsers = Get-ADUser @adParams
        Write-Verbose "Comptes désactivés trouvés : $($disabledUsers.Count)"

        $result = foreach ($user in $disabledUsers) {
            # Parser la date de désactivation depuis le champ Description
            $dateDesactivation = $null
            $joursDesactive    = $null

            if ($user.Description) {
                $match = $datePattern.Match($user.Description)
                if ($match.Success) {
                    $dateDesactivation = [datetime]::ParseExact($match.Value, "yyyy-MM-dd", $null)
                    $joursDesactive    = [int](New-TimeSpan -Start $dateDesactivation -End (Get-Date)).TotalDays
                }
            }

            # Fallback : utiliser whenChanged si pas de date dans Description
            if (-not $dateDesactivation) {
                $dateDesactivation = $user.whenChanged
                $joursDesactive    = [int](New-TimeSpan -Start $user.whenChanged -End (Get-Date)).TotalDays
            }

            # Filtrer par ancienneté minimale
            if ($MinDaysDisabled -gt 0 -and $joursDesactive -lt $MinDaysDisabled) { continue }

            # Récupérer les noms des groupes (hors groupes primaires)
            $groupes = $user.MemberOf | ForEach-Object {
                (Get-ADGroup -Identity $_ -ErrorAction SilentlyContinue).Name
            } | Where-Object { $_ }

            $ou = ($user.DistinguishedName -split ",", 2)[1]

            [PSCustomObject]@{
                UPN               = $user.UserPrincipalName
                SamAccountName    = $user.SamAccountName
                DisplayName       = $user.Name
                DateDesactivation = $dateDesactivation
                JoursDesactive    = $joursDesactive
                Description       = $user.Description
                OU                = $ou
                NombreGroupes     = $groupes.Count
                Groupes           = $groupes -join "; "
                Mail              = $user.mail
            }
        }

        return $result | Sort-Object JoursDesactive -Descending
    }

    end {
        if ($show) { Write-Host "$($PSStyle.Background.Blue)<<< Get-Faitza_Revue_comptes_desactives >>>$($PSStyle.Reset)" }
    }
}

# Comptes désactivés depuis plus de 30 jours
$desactives = Get-Faitza_Revue_comptes_desactives -MinDaysDisabled 30 -show

# Identifier les comptes désactivés qui ont encore des groupes
$desactives | Where-Object { $_.NombreGroupes -gt 0 } |
    Select-Object UPN, JoursDesactive, NombreGroupes, Groupes |
    Format-Table -AutoSize
Groupes persistants = risque réel

Un compte désactivé membre d'un groupe de sécurité reste dans ce groupe. Si le compte est un jour réactivé (erreur, reprise de poste), il récupère instantanément tous ses anciens droits. La bonne pratique est de vider les appartenances aux groupes au moment de la désactivation, et de ne supprimer le compte qu'après un délai de rétention (ex. 90 jours).

Remédiation automatisée — Invoke-Faitza_Action_comptes_inactifs

Invoke-Faitza_Action_comptes_inactifs orchestre les actions de remédiation pour les comptes inactifs détectés : notification email au manager, désactivation après délai, et génération du rapport. Elle peut être exécutée en mode dry-run pour simuler les actions sans les appliquer.

function Invoke-Faitza_Action_comptes_inactifs {
    <#
    .SYNOPSIS
        Remédie les comptes inactifs : notification manager, désactivation, rapport.
    .PARAMETER Comptes
        Tableau de comptes retourné par Get-Faitza_Revue_comptes_inactifs.
    .PARAMETER Phase
        "Notify" : envoyer l'email au manager uniquement.
        "Disable" : désactiver les comptes (à utiliser après un 2e cycle de revue).
        "Report"  : générer uniquement le rapport Excel.
    .PARAMETER WhatIf
        Simuler les actions sans les appliquer (dry-run).
    .PARAMETER SmtpServer
        Serveur SMTP pour les notifications.
    .PARAMETER FromAddress
        Adresse expéditrice des emails de notification.
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)][PSCustomObject[]]$Comptes,
        [ValidateSet("Notify", "Disable", "Report")][string]$Phase = "Notify",
        [string]$SmtpServer  = "smtp.contoso.com",
        [string]$FromAddress = "[email protected]",
        [string]$ReportPath  = ".\Revue_Inactifs_$(Get-Date -Format 'yyyyMMdd').xlsx",
        [switch]$show
    )

    begin {
        if ($show) { Write-Host "$($PSStyle.Background.Blue)>>> Invoke-Faitza_Action_comptes_inactifs <<<$($PSStyle.Reset)" }
        $stats = @{ Notified = 0; Disabled = 0; Errors = 0 }
    }

    process {
        switch ($Phase) {

            "Notify" {
                Write-Host "Phase : Notification managers ($($Comptes.Count) comptes)" -ForegroundColor Cyan

                # Grouper les comptes par manager pour envoyer un seul email par manager
                $byManager = $Comptes | Where-Object { $_.Manager } | Group-Object Manager

                foreach ($group in $byManager) {
                    $managerUpn = $group.Name
                    $managed    = $group.Group

                    $body = @"
Bonjour,

Dans le cadre de la revue périodique des accès, les comptes suivants dont vous êtes manager
n'ont pas eu de connexion depuis plus de 90 jours :

$(($managed | ForEach-Object { "  - $($_.UPN) (dernière connexion : $($_.LastLogon?.ToString('yyyy-MM-dd') ?? 'jamais'))" }) -join "`n")

Ces comptes seront désactivés dans 14 jours si aucune action n'est prise.
Pour conserver un compte, répondez à cet email en justifiant le maintien.

Cordialement,
L'équipe Sécurité IT — faitza.com
"@

                    if ($PSCmdlet.ShouldProcess($managerUpn, "Envoyer notification d'inactivité")) {
                        try {
                            Send-MailMessage `
                                -To         $managerUpn `
                                -From       $FromAddress `
                                -Subject    "[Revue IAM] Comptes inactifs sous votre responsabilité" `
                                -Body       $body `
                                -SmtpServer $SmtpServer `
                                -Encoding   UTF8

                            $stats.Notified += $managed.Count
                            Write-Host "  [OK] Email envoyé à : $managerUpn ($($managed.Count) compte(s))" -ForegroundColor Green
                        }
                        catch {
                            $stats.Errors++
                            Write-Warning "Impossible d'envoyer l'email à $managerUpn : $($_.Exception.Message)"
                        }
                    }
                }
            }

            "Disable" {
                Write-Host "Phase : Désactivation ($($Comptes.Count) comptes)" -ForegroundColor Yellow

                foreach ($compte in $Comptes) {
                    if ($PSCmdlet.ShouldProcess($compte.UPN, "Désactiver le compte AD")) {
                        try {
                            # Écrire la date de désactivation dans Description avant de désactiver
                            $desc = "Désactivé le $(Get-Date -Format 'yyyy-MM-dd') — Inactivité > 90j (Revue IAM automatique)"
                            Set-ADUser -Identity $compte.SamAccountName `
                                       -Enabled $false `
                                       -Description $desc

                            # Logger la désactivation
                            Write-Faitza_retour `
                                -message    "Compte désactivé (inactivité) : $($compte.UPN)" `
                                -level      "Warning" `
                                -scriptname "Faitza_Revue"

                            $stats.Disabled++
                            Write-Host "  [OK] Désactivé : $($compte.UPN)" -ForegroundColor Green
                        }
                        catch {
                            $stats.Errors++
                            Write-Warning "Erreur désactivation $($compte.UPN) : $($_.Exception.Message)"
                        }
                    }
                }
            }

            "Report" {
                Write-Host "Phase : Génération du rapport Excel → $ReportPath" -ForegroundColor Cyan
                # Appel au module Faitza_Export (basé sur ImportExcel ou EPPlus)
                $Comptes | Export-Faitza_Excel `
                    -Path        $ReportPath `
                    -SheetName   "Comptes Inactifs" `
                    -Title       "Revue IAM — Comptes Inactifs $(Get-Date -Format 'dd/MM/yyyy')" `
                    -FreezeRow   1 `
                    -AutoFilter `
                    -ConditionalFormat @{
                        Column    = "InactiveDays"
                        HighValue = 365
                        LowColor  = "#FFF2CC"
                        HighColor = "#FF0000"
                    }

                Write-Host "[OK] Rapport généré : $ReportPath" -ForegroundColor Green
            }
        }
    }

    end {
        Write-Host "`n── Résumé ──────────────────────────────────" -ForegroundColor DarkGray
        Write-Host "   Notifiés   : $($stats.Notified)" -ForegroundColor Cyan
        Write-Host "   Désactivés : $($stats.Disabled)" -ForegroundColor Yellow
        Write-Host "   Erreurs    : $($stats.Errors)"   -ForegroundColor Red
        if ($show) { Write-Host "$($PSStyle.Background.Blue)<<< Invoke-Faitza_Action_comptes_inactifs >>>$($PSStyle.Reset)" }
    }
}

# ─── Exemple : cycle complet de revue ────────────────────────────────────────

# 1. Détecter les inactifs
$inactifs = Get-Faitza_Revue_comptes_inactifs -ThresholdDays 90 -CorrelateEntra

# 2. Simuler (WhatIf) pour valider avant d'agir
Invoke-Faitza_Action_comptes_inactifs -Comptes $inactifs -Phase Notify -WhatIf

# 3. Notifier les managers
Invoke-Faitza_Action_comptes_inactifs -Comptes $inactifs -Phase Notify

# 4. Après 14 jours, 2e revue : désactiver les comptes sans réponse
$toDisable = Get-Faitza_Revue_comptes_inactifs -ThresholdDays 90 -CorrelateEntra
Invoke-Faitza_Action_comptes_inactifs -Comptes $toDisable -Phase Disable

# 5. Générer le rapport Excel final
Invoke-Faitza_Action_comptes_inactifs -Comptes $toDisable -Phase Report

Workflow de gouvernance

Cycle de vie complet — Revue IAM

Le workflow se déroule en quatre phases distinctes espacées dans le temps :

J0 — Détection : Get-Faitza_Revue_comptes_inactifs identifie tous les comptes sans connexion depuis 90+ jours. Le rapport est généré et envoyé à l'équipe sécurité.

J0 — Notification : Invoke-Faitza_Action_comptes_inactifs -Phase Notify envoie un email groupé à chaque manager listant ses comptes inactifs. Le manager a 14 jours pour justifier le maintien du compte.

J14 — Désactivation : Une seconde exécution de la détection identifie les comptes qui n'ont toujours pas eu de connexion. -Phase Disable les désactive et inscrit la date dans le champ Description.

J104 — Suppression (J14 + 90) : Get-Faitza_Revue_comptes_desactives -MinDaysDisabled 90 identifie les comptes désactivés depuis 90 jours. Après validation humaine, ils sont supprimés de l'AD.

# Script d'orchestration planifié (tâche planifiée Windows ou Azure Automation Runbook)
# S'exécute chaque semaine. La logique de phase est déterminée par la date courante.

#Requires -Modules Faitza_Revue, Faitza_Log, Faitza_Export, ActiveDirectory, Microsoft.Graph.Users

[CmdletBinding()]
param()

$scriptName = "Faitza_Revue_Orchestration"
$week       = Get-Date -UFormat "%V"   # Numéro de semaine ISO

Write-Faitza_retour -message "Démarrage revue IAM — Semaine $week" -level Info -scriptname $scriptName

try {
    # ── Phase 1 : Rapport inactifs (chaque semaine) ────────────────────────────
    $inactifs = Get-Faitza_Revue_comptes_inactifs -ThresholdDays 90 -CorrelateEntra
    Write-Faitza_retour -message "$($inactifs.Count) comptes inactifs détectés" -level Info -scriptname $scriptName

    Invoke-Faitza_Action_comptes_inactifs -Comptes $inactifs -Phase Report `
        -ReportPath "\\server\Revues\Inactifs_S$week.xlsx"

    # ── Phase 2 : Notification managers (semaines paires uniquement) ───────────
    if ([int]$week % 2 -eq 0) {
        Invoke-Faitza_Action_comptes_inactifs -Comptes $inactifs -Phase Notify
        Write-Faitza_retour -message "Notifications managers envoyées (S$week)" -level Info -scriptname $scriptName
    }

    # ── Phase 3 : Désactivation (après 2 semaines d'inactivité persistante) ────
    # Comptes inactifs depuis 104 jours = 90 + 14 jours de délai de notification
    $aDesactiver = Get-Faitza_Revue_comptes_inactifs -ThresholdDays 104 -CorrelateEntra
    if ($aDesactiver) {
        Invoke-Faitza_Action_comptes_inactifs -Comptes $aDesactiver -Phase Disable
        Write-Faitza_retour -message "$($aDesactiver.Count) comptes désactivés" -level Warning -scriptname $scriptName
    }

    # ── Phase 4 : Rapport comptes désactivés à purger ─────────────────────────
    $desactivesPurge = Get-Faitza_Revue_comptes_desactives -MinDaysDisabled 90
    if ($desactivesPurge) {
        Write-Faitza_retour -message "$($desactivesPurge.Count) comptes désactivés depuis 90+ jours (à supprimer)" `
            -level Warning -scriptname $scriptName

        $desactivesPurge | Export-Faitza_Excel `
            -Path      "\\server\Revues\ASupprimer_S$week.xlsx" `
            -SheetName "Comptes à supprimer"
    }
}
catch {
    Write-Faitza_retour -message "ERREUR orchestration revue IAM : $($_.Exception.Message)" `
        -level Error -scriptname $scriptName
    throw
}
finally {
    Write-Faitza_retour -message "Fin revue IAM — Semaine $week" -level Info -scriptname $scriptName
}

Reporting Excel

Le module Faitza_Export (basé sur le module PowerShell ImportExcel) génère des rapports formatés avec mise en forme conditionnelle, filtres automatiques et en-têtes figés. L'intégration dans la revue IAM produit deux fichiers : un rapport des inactifs et un rapport des comptes à supprimer.

# Exemple de rapport Excel complet avec mise en forme
# (utilise le module ImportExcel : Install-Module ImportExcel)

$inactifs = Get-Faitza_Revue_comptes_inactifs -ThresholdDays 90 -CorrelateEntra
$desactives = Get-Faitza_Revue_comptes_desactives -MinDaysDisabled 0

$reportPath = ".\Revue_IAM_$(Get-Date -Format 'yyyyMMdd').xlsx"

# Onglet 1 — Comptes inactifs
$inactifs | Select-Object UPN, DisplayName, LastLogon, LastEntraSignIn, InactiveDays, Manager, OU |
    Export-Excel -Path $reportPath `
        -WorksheetName  "Inactifs" `
        -TableName      "TblInactifs" `
        -TableStyle     Medium6 `
        -FreezeTopRow `
        -AutoSize `
        -ConditionalText (
            New-ConditionalText -Text 365 -ConditionalType GreaterThan -BackgroundColor "#FF6B6B" -ForegroundColor White,
            New-ConditionalText -Text 180 -ConditionalType GreaterThan -BackgroundColor "#FFB347" -ForegroundColor Black,
            New-ConditionalText -Text 90  -ConditionalType GreaterThan -BackgroundColor "#FFF2CC" -ForegroundColor Black
        ) `
        -Title "Revue IAM — Comptes Inactifs $(Get-Date -Format 'dd/MM/yyyy')" `
        -TitleBold `
        -TitleSize 14

# Onglet 2 — Comptes désactivés
$desactives | Select-Object UPN, DisplayName, DateDesactivation, JoursDesactive, NombreGroupes, Groupes, OU |
    Export-Excel -Path $reportPath `
        -WorksheetName "Désactivés" `
        -TableName     "TblDesactives" `
        -TableStyle    Medium2 `
        -FreezeTopRow `
        -AutoSize `
        -Append    # Ajouter à l'Excel existant, ne pas écraser

Write-Host "[OK] Rapport généré : $reportPath" -ForegroundColor Green
Invoke-Item $reportPath   # Ouvrir dans Excel
Envoi automatique par email

Le rapport Excel peut être envoyé automatiquement à l'équipe sécurité via Send-MailMessage -Attachments $reportPath ou via Microsoft Graph (Send-MgUserMail) pour les environnements sans SMTP relay accessible. Préférez Graph pour les environnements cloud-only.

Pièges classiques

# ─── Piège 1 : lastLogon toujours à 0 ────────────────────────────────────────
# Si lastLogon = 0, le compte n'a JAMAIS été utilisé pour se connecter via Kerberos.
# Un compte de service qui s'authentifie uniquement via NTLM ou via des apps modernes
# (Entra SSO) peut avoir lastLogon = 0 même s'il est actif.
# Solution : toujours corréler avec Entra sign-in logs (-CorrelateEntra).
$raw = [DateTime]::FromFileTime(0)   # → 01/01/1601 00:00:00 = jamais connecté

# ─── Piège 2 : Comptes de service dans les résultats ────────────────────────
# Les comptes de service ne se connectent jamais interactivement et seront toujours
# dans la liste des "inactifs". Filtrez-les via une OU dédiée ou un attribut custom.
$inactifs = Get-Faitza_Revue_comptes_inactifs -ThresholdDays 90 |
    Where-Object { $_.OU -notlike "*OU=ServiceAccounts*" }

# ─── Piège 3 : Manager non résolu = email perdu ──────────────────────────────
# Si le champ Manager de l'objet AD est vide, la notification ne peut pas être envoyée.
# Auditez les managers manquants avant la phase Notify.
$sansManager = $inactifs | Where-Object { -not $_.Manager }
Write-Warning "$($sansManager.Count) comptes sans manager renseigné — notification impossible"

# ─── Piège 4 : Latence de la corrélation multi-DC sur grands environnements ──
# Interroger 10+ DC pour 5000+ utilisateurs peut prendre 10-20 minutes.
# Utilisez des jobs parallèles (ForEach-Object -Parallel) pour accélérer.
$allDCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
$lastLogonByUser = [hashtable]::Synchronized(@{})

$allDCs | ForEach-Object -ThrottleLimit 5 -Parallel {
    $dc   = $_
    $dict = $using:lastLogonByUser
    Get-ADUser -Filter { Enabled -eq $true } -Properties lastLogon -Server $dc |
        ForEach-Object {
            if ($_.lastLogon -gt 0) {
                $dt  = [DateTime]::FromFileTime($_.lastLogon)
                $key = $_.SamAccountName
                $dict[$key] = if ($dict.ContainsKey($key) -and $dict[$key] -gt $dt) { $dict[$key] } else { $dt }
            }
        }
}

# ─── Piège 5 : Désactivation sans log préalable ──────────────────────────────
# Ne jamais désactiver un compte sans avoir écrit la date dans Description ET
# loggé l'action. En cas d'incident post-désactivation, vous devez pouvoir prouver
# qui a désactivé quoi et pourquoi.
Set-ADUser -Identity $sam -Enabled $false `
           -Description "Désactivé le $(Get-Date -Format 'yyyy-MM-dd') — Revue IAM automatique S$(Get-Date -UFormat '%V')"
Ne jamais supprimer sans délai de rétention

La suppression immédiate d'un compte AD est irréversible (hors corbeille AD activée). Imposez toujours un délai de rétention entre la désactivation et la suppression — 90 jours est une valeur courante. Pendant ce délai, le compte est visible dans Get-Faitza_Revue_comptes_desactives et peut être réactivé en cas d'erreur ou de reprise de poste.

Corbeille Active Directory

Si la corbeille AD (AD Recycle Bin) est activée sur le domaine, les objets supprimés restent récupérables pendant la durée de rétention configurée (défaut : 180 jours). Vérifiez avec Get-ADOptionalFeature -Filter { Name -like "Recycle Bin*" }. Sans corbeille, la restauration d'un compte supprimé nécessite une restauration depuis la sauvegarde AD.

// Commentaires

Aucun commentaire pour l'instant.