Tutoriels gratuits et ouverts à toutes et à tous · conçus au Lycée Chevrollier — AngersÀ propos

Série « Parcours PowerShell » · partie 7 / 8

PowerShell niveau 6 — Active Directory et serveurs Windows

Utilisateurs, groupes, unités d’organisation, création en masse et audit de l’annuaire : niveau 6 du parcours PowerShell, avec exercices corrigés et QCM.

Systèmes✎ B. Jousseaume📅 17 octobre 2026⏱ 8 h📶 Avancé

1. Le module ActiveDirectory

# Sur un serveur (contrôleur de domaine ou serveur membre)
Install-WindowsFeature -Name RSAT-AD-PowerShell

# Sur un poste d'administration Windows 10/11
Add-WindowsCapability -Online -Name "Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0"

Import-Module ActiveDirectory
Get-Command -Module ActiveDirectory | Measure-Object        # plus de 140 cmdlets
Get-ADDomain
Get-ADDomainController -Filter *

Le vocabulaire de l’annuaire

TermeSignification
DN (Distinguished Name)Chemin complet d’un objet : CN=Marie Durand,OU=Profs,DC=lycee,DC=local
OUUnité d’organisation : un conteneur, sur lequel on applique des GPO et des délégations
SamAccountNameIdentifiant de connexion court (mdurand)
UPNIdentifiant au format adresse (mdurand@lycee.local)
Groupe de sécuritéPorteur des droits ; on affecte les droits aux groupes, jamais aux comptes

2. Rechercher dans l’annuaire

Get-ADUser -Identity mdurand -Properties *
Get-ADUser -Filter "Department -eq 'Informatique'" -Properties Department, Title

# Rechercher dans une OU précise
Get-ADUser -SearchBase "OU=Eleves,DC=lycee,DC=local" -Filter * |
    Select-Object Name, SamAccountName, Enabled

# Filtre LDAP, plus puissant
Get-ADUser -LDAPFilter "(&(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))"

Get-ADGroup -Filter "Name -like 'GRP_*'"
Get-ADGroupMember -Identity "GRP_Profs" | Select-Object Name, objectClass
Get-ADComputer -Filter * -Properties LastLogonDate | Select-Object Name, OperatingSystem, LastLogonDate

3. Créer et modifier

$motDePasse = Read-Host "Mot de passe initial" -AsSecureString

New-ADUser -Name "Marie Durand" `
    -GivenName "Marie" -Surname "Durand" `
    -SamAccountName "mdurand" `
    -UserPrincipalName "mdurand@lycee.local" `
    -Path "OU=Profs,DC=lycee,DC=local" `
    -AccountPassword $motDePasse `
    -ChangePasswordAtLogon $true `
    -Enabled $true

Set-ADUser -Identity mdurand -Department "Mathématiques" -Title "Professeur"
Add-ADGroupMember -Identity "GRP_Profs" -Members mdurand
Move-ADObject -Identity (Get-ADUser mdurand).DistinguishedName -TargetPath "OU=Archives,DC=lycee,DC=local"
Disable-ADAccount -Identity mdurand
Set-ADAccountPassword -Identity mdurand -Reset -NewPassword (Read-Host -AsSecureString)

La même écriture avec le splatting du niveau 2 est plus lisible et se prête mieux à un script :

$parametres = @{
    Name              = "Marie Durand"
    GivenName         = "Marie"
    Surname           = "Durand"
    SamAccountName    = "mdurand"
    UserPrincipalName = "mdurand@lycee.local"
    Path              = "OU=Profs,DC=lycee,DC=local"
    AccountPassword   = $motDePasse
    ChangePasswordAtLogon = $true
    Enabled           = $true
}
New-ADUser @parametres

Unités d’organisation

New-ADOrganizationalUnit -Name "BTS-SIO" -Path "OU=Eleves,DC=lycee,DC=local" -ProtectedFromAccidentalDeletion $true
Get-ADOrganizationalUnit -Filter * | Select-Object Name, DistinguishedName

L’option -ProtectedFromAccidentalDeletion est activée par défaut et doit le rester : elle a sauvé bien des annuaires.

4. Création en masse depuis un CSV

C’est le scénario type de la rentrée scolaire. Le script suit toujours la même progression : lire, contrôler, simuler, exécuter, journaliser.

# eleves.csv :  Nom;Prenom;Classe
$eleves = Import-Csv "C:\Temp\eleves.csv" -Delimiter ";" -Encoding UTF8
$rapport = foreach ($e in $eleves) {

    $identifiant = (Remove-Accent ($e.Prenom.Substring(0,1) + $e.Nom)).ToLower() -replace "[^a-z0-9]", ""

    if (Get-ADUser -Filter "SamAccountName -eq '$identifiant'") {
        [PSCustomObject]@{ Identifiant = $identifiant; Etat = "Déjà existant" }
        continue
    }

    $ou = "OU=$($e.Classe),OU=Eleves,DC=lycee,DC=local"
    if (-not (Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$ou'")) {
        [PSCustomObject]@{ Identifiant = $identifiant; Etat = "OU manquante : $($e.Classe)" }
        continue
    }

    try {
        New-ADUser -Name "$($e.Prenom) $($e.Nom)" -GivenName $e.Prenom -Surname $e.Nom `
            -SamAccountName $identifiant -UserPrincipalName "$identifiant@lycee.local" `
            -Path $ou -AccountPassword $motDePasseInitial -ChangePasswordAtLogon $true `
            -Enabled $true -ErrorAction Stop
        Add-ADGroupMember -Identity "GRP_Eleves" -Members $identifiant
        [PSCustomObject]@{ Identifiant = $identifiant; Etat = "Créé" }
    }
    catch {
        [PSCustomObject]@{ Identifiant = $identifiant; Etat = "Erreur : $($_.Exception.Message)" }
    }
}

$rapport | Export-Csv "C:\Temp\rapport-creation.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
$rapport | Group-Object Etat | Select-Object Count, Name

5. Auditer l’annuaire

# Comptes actifs jamais connectés depuis 90 jours
$limite = (Get-Date).AddDays(-90)
Get-ADUser -Filter "Enabled -eq '$true'" -Properties LastLogonDate, whenCreated |
    Where-Object { $_.LastLogonDate -lt $limite -or -not $_.LastLogonDate } |
    Select-Object Name, SamAccountName, LastLogonDate, whenCreated |
    Sort-Object LastLogonDate

# Mots de passe qui n'expirent jamais
Get-ADUser -Filter "PasswordNeverExpires -eq '$true' -and Enabled -eq '$true'" |
    Select-Object Name, SamAccountName

# Groupes privilégiés
foreach ($g in "Admins du domaine", "Administrateurs de l'entreprise", "Opérateurs de compte") {
    Get-ADGroupMember -Identity $g -Recursive -ErrorAction SilentlyContinue |
        Select-Object @{N = "Groupe"; E = { $g }}, Name, objectClass
}

# Ordinateurs inactifs
Get-ADComputer -Filter * -Properties LastLogonDate, OperatingSystem |
    Where-Object LastLogonDate -lt (Get-Date).AddDays(-180) |
    Select-Object Name, OperatingSystem, LastLogonDate

6. Côté serveur

Get-WindowsFeature | Where-Object Installed | Select-Object Name, DisplayName
Install-WindowsFeature -Name DHCP -IncludeManagementTools

Get-SmbShare
New-SmbShare -Name "Partage-SIO" -Path "D:\Partages\SIO" -FullAccess "LYCEE\GRP_Profs"
Get-SmbShareAccess -Name "Partage-SIO"

Get-GPO -All | Select-Object DisplayName, GpoStatus, ModificationTime
Get-GPOReport -Name "GPO-Eleves" -ReportType Html -Path C:\Temp\gpo.html

7. Mini-projet : la campagne de rentrée

8. Bilan

Exercices corrigés

Ces corrigés supposent une maquette avec le domaine lycee.local. Adapte les noms distinctifs (DC=…) à ton environnement : c’est l’erreur la plus fréquente lors du premier essai.

Exercice 6.1 — Explorer l’annuaire

Voir le corrigé
# 1.
Get-ADDomain | Select-Object Name, DNSRoot, DistinguishedName
Get-ADDomainController -Filter * | Select-Object Name, IPv4Address, OperatingSystem

# 2.
Get-ADUser -Filter * -Properties Enabled | Group-Object Enabled | Select-Object Name, Count

# 3.
Get-ADUser -SearchBase "OU=Eleves,DC=lycee,DC=local" -Filter * -Properties LastLogonDate |
    Select-Object Name, SamAccountName, LastLogonDate | Sort-Object LastLogonDate

# 4.
Get-ADGroupMember "Admins du domaine" |
    Get-ADUser -Properties LastLogonDate |
    Select-Object Name, SamAccountName, LastLogonDate

Explication

Group-Object Enabled donne en une ligne le décompte actifs/désactivés. Le chaînage Get-ADGroupMember | Get-ADUser -Properties LastLogonDate fonctionne car les objets renvoyés portent l’identité attendue par la cmdlet suivante.

Exercice 6.2 — Rentrée des classes

Voir le corrigé
[CmdletBinding()]
param(
    [Parameter(Mandatory)][string]$FichierCsv,
    [string]$OuRacine = "OU=Eleves,DC=lycee,DC=local",
    [switch]$Simulation
)

function Remove-Accent {
    param([string]$Texte)
    ($Texte.Normalize([Text.NormalizationForm]::FormD) -replace "\p{Mn}", "")
}

$motDePasse = Read-Host "Mot de passe initial commun (maquette uniquement)" -AsSecureString
$eleves = Import-Csv $FichierCsv -Delimiter ";" -Encoding UTF8

$rapport = foreach ($e in $eleves) {
    if (-not $e.Nom -or -not $e.Prenom -or -not $e.Classe) {
        [PSCustomObject]@{ Ligne = "$($e.Nom) $($e.Prenom)"; Etat = "Ligne incomplète" }
        continue
    }

    $base = (Remove-Accent ($e.Prenom.Substring(0,1) + $e.Nom)).ToLower() -replace "[^a-z0-9]", ""
    $identifiant = $base
    $n = 1
    while (Get-ADUser -Filter "SamAccountName -eq '$identifiant'" -ErrorAction SilentlyContinue) {
        $n++
        $identifiant = "$base$n"
    }

    $ouClasse = "OU=$($e.Classe),$OuRacine"
    $groupe = "GRP_$($e.Classe)"

    if ($Simulation) {
        [PSCustomObject]@{ Identifiant = $identifiant; Ou = $ouClasse; Groupe = $groupe; Etat = "Simulation" }
        continue
    }

    if (-not (Get-ADOrganizationalUnit -Filter "DistinguishedName -eq '$ouClasse'")) {
        New-ADOrganizationalUnit -Name $e.Classe -Path $OuRacine
    }
    if (-not (Get-ADGroup -Filter "Name -eq '$groupe'")) {
        New-ADGroup -Name $groupe -GroupScope Global -GroupCategory Security -Path $ouClasse
    }

    try {
        New-ADUser -Name "$($e.Prenom) $($e.Nom)" -GivenName $e.Prenom -Surname $e.Nom `
            -SamAccountName $identifiant -UserPrincipalName "$identifiant@lycee.local" `
            -Path $ouClasse -AccountPassword $motDePasse -ChangePasswordAtLogon $true `
            -Enabled $true -ErrorAction Stop
        Add-ADGroupMember -Identity $groupe -Members $identifiant
        [PSCustomObject]@{ Identifiant = $identifiant; Ou = $ouClasse; Groupe = $groupe; Etat = "Créé" }
    }
    catch {
        [PSCustomObject]@{ Identifiant = $identifiant; Ou = $ouClasse; Groupe = $groupe; Etat = "Erreur : $($_.Exception.Message)" }
    }
}

$rapport | Export-Csv "rapport-rentree.csv" -Delimiter ";" -Encoding UTF8 -NoTypeInformation
$rapport | Group-Object Etat | Select-Object Count, Name

Explication

Le script cherche un identifiant libre dans l’annuaire, et non dans le seul fichier : c’est indispensable puisque des comptes existent déjà. Le mode simulation renvoie exactement les mêmes objets que le mode réel, ce qui permet de comparer avant d’agir.

Pour aller plus loin

Ajoute une colonne Etat dans un fichier de reprise : en relançant le script avec ce fichier, tu ne retraites que les lignes en erreur.

Exercice 6.3 — Audit

Voir le corrigé
# 1.
$limite = (Get-Date).AddDays(-90)
Get-ADUser -Filter "Enabled -eq '$true'" -Properties LastLogonDate, whenCreated |
    Where-Object { -not $_.LastLogonDate -or $_.LastLogonDate -lt $limite } |
    Select-Object Name, SamAccountName, LastLogonDate, whenCreated

# 2.
"Admins du domaine", "Administrateurs de l'entreprise", "Administrateurs du schéma" | ForEach-Object {
    $groupe = $_
    Get-ADGroupMember -Identity $groupe -Recursive -ErrorAction SilentlyContinue |
        Select-Object @{N = "Groupe"; E = { $groupe }}, Name, objectClass
}

# 3.
Get-ADUser -Filter * -Properties Description |
    Where-Object { $_.Description -match "(?i)(mot de passe|mdp|pass)" } |
    Select-Object Name, SamAccountName, Description

# 4.
$tous = Get-ADUser -Filter * -Properties Enabled, LastLogonDate
[PSCustomObject]@{
    Actifs       = ($tous | Where-Object Enabled).Count
    Desactives   = ($tous | Where-Object { -not $_.Enabled }).Count
    Dormants90j  = ($tous | Where-Object { $_.Enabled -and (-not $_.LastLogonDate -or $_.LastLogonDate -lt $limite) }).Count
    Admins       = (Get-ADGroupMember "Admins du domaine" -Recursive).Count
}

Explication

-Recursive sur Get-ADGroupMember déplie les groupes imbriqués : un compte peut être administrateur sans figurer directement dans le groupe. C’est précisément ce que cherchent les attaquants, et ce que les audits oublient.

Pour aller plus loin

# Version indépendante de la langue, par identifiant de sécurité bien connu
$domaine = (Get-ADDomain).DomainSID.Value
Get-ADGroup -Identity "$domaine-512" | Get-ADGroupMember -Recursive

Mini-projet

Voir le corrigé
function New-MotDePasse {
    param([int]$Longueur = 14)
    $alphabet = [char[]]"abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%*"
    -join (1..$Longueur | ForEach-Object { $alphabet | Get-Random })
}

# Dans la boucle de création :
$clair = New-MotDePasse
$securise = ConvertTo-SecureString $clair -AsPlainText -Force
New-ADUser @parametres -AccountPassword $securise -ChangePasswordAtLogon $true

# Fichier de remise, à imprimer puis détruire
[PSCustomObject]@{ Identifiant = $identifiant; MotDePasseInitial = $clair } |
    Export-Csv "remise-eleves.csv" -Append -Delimiter ";" -Encoding UTF8 -NoTypeInformation

Ce fichier de remise est le point sensible du script : il contient des mots de passe en clair. Il doit être créé dans un dossier à accès restreint, imprimé puis supprimé immédiatement, et jamais envoyé par messagerie. Comme les comptes exigent un changement à la première connexion, sa durée de vie utile est de quelques heures.

Explication

Un mot de passe différent par élève est la seule approche acceptable : un mot de passe commun signifie que chacun peut se connecter au compte des autres pendant les premières heures. L’alphabet exclut les caractères ambigus (I, l, 1, O, 0) pour limiter les erreurs de saisie lors de la remise.

Pour aller plus loin

Pour aller plus loin, génère un courrier de remise individuel (un fichier par élève) plutôt qu’un tableau global : la distribution est alors plus facile à cloisonner.

🧠 Testez vos connaissances

5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.

1. Pourquoi écrire Get-ADUser -Filter "Enabled -eq '$true'" plutôt que Get-ADUser -Filter * | Where-Object Enabled ?

💬 Sur plusieurs milliers de comptes, la différence de performance est considérable.

2. Pourquoi LastLogonDate est-elle parfois vide ?

💬 Par défaut, Get-ADUser ne renvoie qu’une dizaine de propriétés : les autres se demandent explicitement.

3. Dans quel ordre mener une création de comptes en masse ?

💬 Le rapport final, ligne par ligne, permet de ne relancer que les échecs.

4. Que ne doit-on jamais écrire dans le champ Description d’un compte AD ?

💬 Ce champ est lisible par tout utilisateur authentifié du domaine : c’est une découverte classique lors des audits.

5. Pourquoi -Recursive est-il important sur Get-ADGroupMember "Admins du domaine" ?

💬 Un compte peut être administrateur via un groupe inclus dans un autre, sans apparaître directement.

© 2026 Bruno Jousseaume. Ce cours est publié sous licence CC BY-NC-SA 4.0 : vous pouvez le partager et l’adapter en citant l’auteur et un lien vers cette page, sans usage commercial, et en partageant vos adaptations sous la même licence.

Pour aller plus loin

Bastion Guacamole : dépanner les connexions RDP

Une démarche méthodique pour diagnostiquer une connexion RDP qui échoue à travers le bastion : journal de guacd, tableau de diagnostic, NLA et NTLM, réseau, fiche de suivi d’incident.

Avancé⏱ 1 h 30 · 8 octobre 2026