1. Exécuter à distance
# Sur la machine distante, une fois (en administrateur)
Enable-PSRemoting -Force
# Commande ponctuelle sur une ou plusieurs machines
Invoke-Command -ComputerName SRV1, SRV2 -ScriptBlock { Get-Service -Name Spooler }
# Session interactive
Enter-PSSession -ComputerName SRV1
Exit-PSSession
# Session persistante : plus efficace pour plusieurs commandes
$session = New-PSSession -ComputerName SRV1
Invoke-Command -Session $session -ScriptBlock { $processus = Get-Process }
Invoke-Command -Session $session -ScriptBlock { $processus.Count }
Remove-PSSession $session
# Variables locales dans un bloc distant : il faut les transmettre
$nomService = "Spooler"
Invoke-Command -ComputerName SRV1 -ScriptBlock {
param($s)
Get-Service -Name $s
} -ArgumentList $nomService
# Ou, plus lisible (PowerShell 3+)
Invoke-Command -ComputerName SRV1 -ScriptBlock { Get-Service -Name $using:nomService }
2. Les identifiants
# Saisie interactive : la seule méthode acceptable en usage courant
$cred = Get-Credential -UserName "LYCEE\adm-bruno" -Message "Compte d'administration"
Invoke-Command -ComputerName SRV1 -Credential $cred -ScriptBlock { hostname }
# Ce que contient l'objet
$cred.UserName
$cred.Password # System.Security.SecureString
Le module SecretManagement
Pour un script automatisé qui ne peut pas demander de saisie, Microsoft fournit un coffre :
Install-Module Microsoft.PowerShell.SecretManagement, Microsoft.PowerShell.SecretStore -Scope CurrentUser
Register-SecretVault -Name CoffreLocal -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name "CompteSauvegarde" -Secret (Get-Credential)
$cred = Get-Secret -Name "CompteSauvegarde"
Get-SecretInfo
3. Planifier un script
$action = New-ScheduledTaskAction -Execute "pwsh.exe" `
-Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Scripts\Clean-Temp.ps1" -Dossier C:\Temp'
$declencheur = New-ScheduledTaskTrigger -Daily -At 6:30am
$parametres = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit (New-TimeSpan -Hours 1)
Register-ScheduledTask -TaskName "Nettoyage quotidien" -Action $action -Trigger $declencheur `
-Settings $parametres -User "SYSTEM" -RunLevel Highest -Description "Nettoie C:\Temp"
Get-ScheduledTask -TaskName "Nettoyage*" | Get-ScheduledTaskInfo # dernière exécution, résultat
4. Faire un module de ses fonctions
Un module rassemble des fonctions réutilisables, importables par Import-Module.
# Arborescence
# C:\Scripts\Modules\LycTools\
# LycTools.psm1 <- les fonctions
# LycTools.psd1 <- le manifeste
# Dans LycTools.psm1
function Get-LycEspaceDisque { ... }
function Write-LycJournal { ... }
Export-ModuleMember -Function Get-LycEspaceDisque, Write-LycJournal
New-ModuleManifest -Path "C:\Scripts\Modules\LycTools\LycTools.psd1" `
-RootModule "LycTools.psm1" -ModuleVersion "1.0.0" -Author "Bruno Jousseaume" `
-Description "Outils d'administration du lycée" -PowerShellVersion "5.1"
$env:PSModulePath -split ";" # où PowerShell cherche les modules
Import-Module LycTools -Force
Get-Command -Module LycTools
5. Consommer une API REST
# GET : Invoke-RestMethod convertit automatiquement le JSON en objets
$reponse = Invoke-RestMethod -Uri "https://api.exemple.fr/v1/postes" -Method Get
$reponse.data | Select-Object nom, ip | Format-Table
# Avec authentification par jeton
$entetes = @{ Authorization = "Bearer $jeton"; "Content-Type" = "application/json" }
Invoke-RestMethod -Uri "https://api.exemple.fr/v1/tickets" -Headers $entetes
# POST avec un corps JSON
$corps = @{ titre = "Imprimante HS"; salle = "207"; priorite = 2 } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.exemple.fr/v1/tickets" -Method Post -Headers $entetes -Body $corps
# Gestion d'erreur HTTP
try {
$r = Invoke-RestMethod -Uri $url -Headers $entetes -ErrorAction Stop
}
catch {
Write-Warning "Code HTTP : $($_.Exception.Response.StatusCode.value__)"
}
6. Projet final : la supervision du parc
7. Et après ?
| Direction | Pour faire quoi | À explorer |
|---|---|---|
| Infrastructure as code | Décrire une configuration et la faire appliquer | DSC, Ansible côté Windows |
| Cloud | Administrer Microsoft 365 et Azure | Modules Microsoft.Graph, Az |
| Supervision | Alimenter un outil de supervision | Zabbix, Prometheus, API REST |
| Sécurité défensive | Chasse aux menaces, réponse à incident | Journalisation PowerShell, Sysmon, JEA |
| Développement | Modules testés et publiés | Pester (tests), PSScriptAnalyzer, Git |
8. Bilan du parcours
Exercices corrigés
Les corrigés de ce niveau sont des squelettes de production : ils montrent la structure attendue plus que la solution unique. Adapte-les à ta maquette et complète-les progressivement.
Exercice 7.1 — À distance
Voir le corrigé
# 1.
Enable-PSRemoting -Force # sur la machine cible
Test-WSMan -ComputerName SRV1
# 2.
Invoke-Command -ComputerName SRV1, SRV2 -ScriptBlock {
Get-PSDrive C | Select-Object @{N = "LibreGo"; E = { [math]::Round($_.Free / 1GB, 1) }}
} | Select-Object PSComputerName, LibreGo
# 3.
function Get-EtatServeurs {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string[]]$Machines,
[pscredential]$Credential
)
foreach ($m in $Machines) {
if (-not (Test-Connection -ComputerName $m -Count 1 -Quiet)) {
[PSCustomObject]@{ Machine = $m; Joignable = $false }
continue
}
try {
$p = @{ ComputerName = $m; ErrorAction = "Stop" }
if ($Credential) { $p.Credential = $Credential }
Invoke-Command @p -ScriptBlock {
$os = Get-CimInstance Win32_OperatingSystem
[PSCustomObject]@{
Machine = $env:COMPUTERNAME
Joignable = $true
UptimeJours = (New-TimeSpan -Start $os.LastBootUpTime).Days
CLibreGo = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
ServicesKo = (Get-Service | Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" }).Count
}
} | Select-Object Machine, Joignable, UptimeJours, CLibreGo, ServicesKo
}
catch {
[PSCustomObject]@{ Machine = $m; Joignable = $false; Erreur = $_.Exception.Message }
}
}
}
Explication
La fonction traite le cas de la machine injoignable avant d’essayer le remoting, ce qui évite un long délai d’attente. Les machines en échec figurent dans le résultat : un tableau de supervision doit montrer ce qui ne répond pas, pas l’omettre.
Pour aller plus loin
Invoke-Command traite par défaut 32 machines en parallèle ; -ThrottleLimit ajuste ce nombre selon la charge acceptable.
Exercice 7.2 — Automatiser
Voir le corrigé
# 1.
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument '-NoProfile -NonInteractive -File "C:\Scripts\Clean-Temp.ps1" -Dossier C:\Temp -JoursAnciennete 30'
$trigger = New-ScheduledTaskTrigger -Daily -At 6:30am
Register-ScheduledTask -TaskName "Nettoyage quotidien" -Action $action -Trigger $trigger `
-User "SYSTEM" -RunLevel Highest
# 2.
Get-ScheduledTask -TaskName "Nettoyage quotidien" | Get-ScheduledTaskInfo |
Select-Object LastRunTime, LastTaskResult, NextRunTime
# LastTaskResult = 0 signifie succès
# 3.
Start-ScheduledTask -TaskName "Nettoyage quotidien"
Start-Sleep -Seconds 10
Get-Content "C:\Scripts\clean-temp.log" -Tail 20
Explication
-NoProfile -NonInteractive sont les deux options qui évitent les mauvaises surprises en tâche planifiée : pas de profil personnel, et échec immédiat si le script attend une saisie. LastTaskResult vaut 0 en cas de succès.
Pour aller plus loin
Pour un serveur, préfère un compte de service dédié au compte SYSTEM dès que le script accède à des ressources réseau : SYSTEM se présente comme le compte machine.
Exercice 7.3 — Interroger une API
Voir le corrigé
function Get-Meteo {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Ville,
[string]$BaseUri = "https://api.exemple-meteo.fr/v1/current"
)
Write-Verbose "Interrogation pour $Ville"
try {
$r = Invoke-RestMethod -Uri "$BaseUri?q=$([uri]::EscapeDataString($Ville))" -ErrorAction Stop -TimeoutSec 10
}
catch {
Write-Warning "Appel impossible pour $Ville : $($_.Exception.Message)"
return [PSCustomObject]@{ Ville = $Ville; TemperatureC = $null; Description = "indisponible" }
}
if (-not $r.current) {
Write-Warning "Réponse inattendue pour $Ville"
return
}
[PSCustomObject]@{
Ville = $Ville
TemperatureC = $r.current.temp_c
Description = $r.current.condition.text
Releve = Get-Date
}
}
Explication
La fonction renvoie un objet même en cas d’échec, plutôt que rien : l’appelant peut ainsi distinguer « indisponible » de « aucune donnée ». EscapeDataString protège contre les caractères spéciaux d’une ville saisie par l’utilisateur.
Projet final — Le pendu
Voir le corrigé
<#
.SYNOPSIS
Collecte l'état d'un parc de machines et produit un rapport HTML.
.EXAMPLE
.\Get-RapportParc.ps1 -Machines SRV1,SRV2 -SeuilDisquePct 20
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string[]]$Machines,
[string]$CheminRapport = "$PSScriptRoot\rapport-parc.html",
[int]$SeuilDisquePct = 15,
[string]$Journal = "$PSScriptRoot\rapport-parc.log"
)
function Write-Journal {
param([string]$Message, [ValidateSet("INFO","ALERTE","ERREUR")][string]$Niveau = "INFO")
"{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Niveau, $Message |
Add-Content $Journal -Encoding utf8
}
if ($Machines.Count -eq 1 -and (Test-Path $Machines[0])) {
$Machines = Get-Content $Machines[0] | Where-Object { $_ -and $_ -notmatch "^#" }
}
$collecte = {
$os = Get-CimInstance Win32_OperatingSystem
$disques = Get-CimInstance Win32_LogicalDisk -Filter "DriveType = 3" |
Select-Object DeviceID,
@{N = "LibrePct"; E = { [math]::Round($_.FreeSpace / $_.Size * 100, 1) }}
[PSCustomObject]@{
Machine = $env:COMPUTERNAME
Systeme = $os.Caption
UptimeJours = (New-TimeSpan -Start $os.LastBootUpTime).Days
Disques = $disques
ServicesKo = (Get-Service | Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" }).Name
Erreurs7j = (Get-WinEvent -FilterHashtable @{LogName="System";Level=2;StartTime=(Get-Date).AddDays(-7)} -ErrorAction SilentlyContinue).Count
}
}
Write-Journal "Début de collecte sur $($Machines.Count) machine(s)"
$resultats = foreach ($m in $Machines) {
try {
Invoke-Command -ComputerName $m -ScriptBlock $collecte -ErrorAction Stop -ThrottleLimit 16
}
catch {
Write-Journal "Injoignable : $m ($($_.Exception.Message))" -Niveau ALERTE
[PSCustomObject]@{ Machine = $m; Systeme = "injoignable"; UptimeJours = $null; ServicesKo = @(); Erreurs7j = $null }
}
}
$synthese = foreach ($r in $resultats) {
[PSCustomObject]@{
Machine = $r.Machine
Systeme = $r.Systeme
UptimeJours = $r.UptimeJours
DisquesAlerte = ($r.Disques | Where-Object LibrePct -lt $SeuilDisquePct).DeviceID -join ", "
ServicesKo = $r.ServicesKo.Count
Erreurs7j = $r.Erreurs7j
Anomalie = [bool](($r.Disques | Where-Object LibrePct -lt $SeuilDisquePct) -or $r.ServicesKo.Count -gt 0 -or $r.Systeme -eq "injoignable")
}
}
$style = "<style>body{font-family:Segoe UI,sans-serif;margin:2rem}table{border-collapse:collapse}
th{background:#1A1A1A;color:#fff;padding:6px 10px;text-align:left}td{border-bottom:1px solid #ccc;padding:4px 10px}
tr:has(td:last-child:contains('True')){background:#FBEAE8}</style>"
$synthese | ConvertTo-Html -Head $style -Title "État du parc" |
Out-File $CheminRapport -Encoding utf8
$synthese | Export-Csv ($CheminRapport -replace "\.html$", ".csv") -Delimiter ";" -Encoding UTF8 -NoTypeInformation
$nbAnomalies = ($synthese | Where-Object Anomalie).Count
Write-Journal "Fin de collecte : $nbAnomalies anomalie(s)"
$synthese
if ($nbAnomalies -gt 0) { exit 1 } else { exit 0 }
Le code de retour (exit 1) permet à la tâche planifiée ou à un outil de supervision de détecter automatiquement qu’une anomalie a été trouvée. La mise en évidence CSS des lignes en anomalie est ici simplifiée : en pratique, on construit le tableau HTML soi-même ou on ajoute une classe avec un -replace sur le fragment généré.
Explication
Le script sépare nettement trois responsabilités : collecter (le bloc $collecte, exécuté à distance), synthétiser (la construction des objets de résumé), restituer (HTML, CSV, journal, code de retour). Cette séparation permet de tester chaque partie isolément.
Pour aller plus loin
Étape suivante naturelle : déposer le CSV historique dans un dossier et tracer l’évolution de l’espace disque dans le temps. C’est le point de départ d’une vraie supervision.
🧠 Testez vos connaissances
5 questions. Répondez puis cliquez sur « Corriger ». Rien n’est enregistré : c’est de l’auto-évaluation.
© 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.