Revue périodique des comptes AD/Entra avec PowerShell
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.
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 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
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
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
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')"
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.
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.