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

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

PowerShell niveau 7 — Automatisation avancée

Remoting, identifiants protégés, tâches planifiées, modules et API REST : niveau 7 du parcours PowerShell, avec exercices corrigés et QCM.

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

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 ?

DirectionPour faire quoiÀ explorer
Infrastructure as codeDécrire une configuration et la faire appliquerDSC, Ansible côté Windows
CloudAdministrer Microsoft 365 et AzureModules Microsoft.Graph, Az
SupervisionAlimenter un outil de supervisionZabbix, Prometheus, API REST
Sécurité défensiveChasse aux menaces, réponse à incidentJournalisation PowerShell, Sysmon, JEA
DéveloppementModules testés et publiésPester (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.

1. Que récupère-t-on d’un Invoke-Command distant ?

💬 La propriété PSComputerName, ajoutée automatiquement, indique de quelle machine vient chaque résultat.

2. Comment utiliser une variable locale dans un bloc de script distant ?

💬 Le bloc s’exécute sur la machine distante, qui ne connaît pas ton contexte local.

3. Quelle est la meilleure solution pour un script automatisé qui doit s’authentifier en domaine ?

💬 Le mot de passe est géré par Active Directory et n’est connu de personne. À défaut, on utilise un coffre SecretManagement.

4. Pourquoi ajouter -NoProfile -NonInteractive dans une tâche planifiée ?

💬 Sans -NonInteractive, un script qui attend une saisie reste bloqué indéfiniment, sans trace visible.

5. Face à un certificat TLS invalide lors d’un appel d’API, que faire ?

💬 Désactiver la vérification ouvre la porte à l’interception : c’est un contournement, pas une correction.

© 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