WSUS est déprécié, mais reste pris en charge selon le cycle de vie de Windows Server. Il tourne encore chez une bonne partie de nos clients. Le pourcentage affiché peut donner une lecture trompeuse si on ne précise pas ce qu’il mesure. Voici comment j’ai repris un rapport surévalué et automatisé son envoi mensuel, sans SMTP ni mot de passe stocké.
Le client n’est pas nommé ; la boîte rapports@example.com, les identifiants et les chemins présentés sont des exemples. Les chiffres agrégés sont ceux du cas décrit. Les commandes ci-dessous sont des extraits explicatifs, pas les trois scripts complets.
Le besoin : un rapport mensuel lisible
Je travaille comme administrateur Microsoft chez un MSP. Un de nos clients, un établissement de 266 machines déclarées dans WSUS, voulait un état des lieux de ses mises à jour. Pas un export brut : une synthèse lisible, avec le parc classé par diagnostic et une ligne d'explication pour chaque catégorie.
Un premier rapport avait été fait à la main, et il affichait fièrement 90,6 % de conformité sur le parc actif. Puis la demande est tombée, par Teams : « est-ce que tu peux faire un script pour que je les reçoive régulièrement ? Genre le dernier jeudi du mois ? »
Avant d'écrire quoi que ce soit, je suis allé voir ce qui existait déjà sur le serveur WSUS. Sur un serveur hérité, il y a toujours des scripts. La vraie question, c'est ce qu'ils font réellement.
Ce que j’ai trouvé sur le serveur
J'ai trouvé quatre scripts, tous à lancer à la main, aucun planifié :
-
un export CSV du « % installé » par poste ;
-
un diagnostic pour zoomer sur une machine ;
-
une série de Invoke-WsusServerCleanup ;
-
un DBCC SHRINKDATABASE sur la SUSDB.
Aucun ne définissait $wsus. Ils supposaient tous que quelqu’un avait tapé Get-WsusServer avant. Le diagnostic utilisait une propriété ComputerTargets qui n’existe pas sur un groupe WSUS, et ne pouvait donc pas fonctionner tel quel. Le shrink, lui, risquait de fragmenter les index : ce n’est pas une opération de maintenance à lancer systématiquement. Mais le plus intéressant, c’était l’export.
Pourquoi le pourcentage était trompeur
Le script utilisait un indicateur combinant mises à jour installées et non applicables. Cette formule ne mesure pas la proportion de machines entièrement à jour. Voici le calcul trouvé dans l’ancien export, avec son nom de propriété erroné :
$numerateur = $summary.InstalledCount + $summary.NotApplicableCount
$denominateur = $summary.InstalledCount + $summary.NotApplicableCount + $summary.NotInstalledCount + $summary.FailedCount + $summary.DownloadedCount + $summary.PendingRebootCount
En creusant, j'ai trouvé quatre problèmes.
-
Les MAJ non applicables sont comptées comme réussies. Un poste a des milliers de MAJ non applicables pour quelques dizaines d'applicables. Tout le monde sort à 95-99 %, même un poste avec dix correctifs critiques en retard.
-
Une machine qui n'a jamais rien remonté sort à 100 %. Tous ses compteurs sont à zéro, et le script renvoyait 100 quand le dénominateur valait zéro. Les machines mortes depuis un an étaient donc parfaitement conformes.
-
Un nom de propriété erroné. La propriété du résumé WSUS est
InstalledPendingRebootCount, pasPendingRebootCount. Dans ce script sans mode strict, l’accès à la propriété absente renvoyait$nullet le calcul ignorait les redémarrages en attente. Ce comportement PowerShell change notamment avecSet-StrictMode. -
Un périmètre de mises à jour non défini. L’export ne limitait pas explicitement les résumés aux approbations voulues et ignorait
UnknownCount. Un état inconnu ne doit pas disparaître du rapport. Les propriétés documentées des résumés WSUS permettent de distinguer ces états.
Deux dates pour orienter le diagnostic
Le script ne regardait que LastReportedStatusTime, la dernière fois que le poste a remonté son état. Or WSUS stocke aussi LastSyncTime, la dernière fois que le poste a contacté le serveur. C'est la combinaison des deux qui permet un vrai diagnostic :
-
contact ancien : la machine est éteinte, nomade ou décommissionnée ;
-
contact récent mais rapport ancien : le poste contacte WSUS sans fournir un état récent. Je le classe comme « zombie » pour l’investigation ; un souci d’agent ou un identifiant client dupliqué sont des pistes, pas un diagnostic prouvé par ces deux dates seules.
Avec une seule date, les zombies passent inaperçus.
Choisir un envoi sans SMTP
Restait l’envoi automatique. Send-MailMessage est obsolète ; je voulais éviter une nouvelle dépendance à SMTP et à des identifiants enregistrés. Je n’avais pas d’accès admin au tenant du client, ce qui écartait une application ou un connecteur de relais à configurer de son côté.
J’ai écarté le Direct Send, l’envoi SMTP non authentifié vers le MX du tenant : il dépend notamment du port 25 sortant, du routage et des règles antispam du destinataire. Son autorisation peut aussi changer côté tenant.
J’ai retenu un envoi via Microsoft Graph depuis le tenant de mon employeur. Une permission applicative Mail.Send accordée globalement dans Entra peut donner une portée très large. La RBAC pour applications d’Exchange Online permet ici de limiter le rôle d’envoi à la seule boîte prévue.
Un classement en cascade
J’ai réécrit le rapport autour de sept catégories exclusives, attribuées dans l’ordre suivant. Les compteurs et les dates doivent être collectés et validés avant ce classement :
$cat = if ($null -eq $jContact -or $jContact -gt $SeuilFantomeJours) { 1 } # Fantôme
elseif ($jContact -gt $SeuilActifJours) { 3 } # Injoignable
elseif ($null -eq $jRapport -or $jRapport -gt $SeuilActifJours) { 2 } # Zombie
elseif ($ech -gt 0) { 4 } # En échec
elseif (($manq + $tel + $reboot) -gt 0) { 5 } # À patcher
elseif ($inc -gt 0) { 6 } # État incertain
else { 7 } # Sain
Cet extrait suppose un résumé WSUS valide. Une collecte absente ou un dénominateur nul doit être signalé comme non évalué, jamais transformé automatiquement en « sain » ou en 100 %. Les numéros sont des identifiants de catégorie : ils ne décrivent pas un classement numérique de gravité.
Les catégories 4 à 7 forment le parc actif dans ce rapport. Les seuils, 7 et 180 jours par défaut, sont des paramètres. Le seuil « fantôme » pourra être adapté à la politique de nettoyage retenue ; modifier ce seuil ne supprime aucune machine de WSUS.
Définir les compteurs et leur périmètre
Pour les compteurs, je définis un périmètre de mises à jour et récupère les résumés de toutes les machines en un appel à l’API WSUS, au lieu d’un appel par machine :
$scopeMaj = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$scopeMaj.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
$resumes = $wsus.GetSummariesPerComputerTarget($scopeMaj, $scopeMachines)
Ce filtre sélectionne les révisions approuvées dans le périmètre WSUS. Il ne signifie pas, à lui seul, que chaque mise à jour est approuvée pour chaque groupe de chaque poste : ce point doit être aligné avec les règles d’approbation du serveur. $wsus et $scopeMachines représentent respectivement la connexion au serveur et le ComputerTargetScope préparés par le script. La méthode Microsoft précise ce double périmètre.
La conformité d’une machine devient « installées / applicables », sans les non applicables, et n’est calculée que pour les machines actives. Les états inconnus sont signalés à part : ils ne prouvent pas une absence de correctifs manquants. Au niveau du parc, j’affiche deux taux plutôt qu’un :
-
par machine : machines saines / machines actives ;
-
par MAJ : MAJ installées / MAJ applicables sur le parc actif.
Les deux indicateurs ne racontent pas la même histoire. Sur ce parc, seules 118 des 179 machines actives étaient saines, soit 65,9 %. On était loin des 90,6 % du premier rapport, qui utilisait une autre définition. Un taux élevé sur les couples poste/mise à jour ne prouve pas qu’autant de machines sont entièrement à jour.
Le résultat sort en CSV séparé par des points-virgules, pour qu'Excel en français l'ouvre correctement. Le script est enregistré en UTF-8 avec BOM, sinon PowerShell 5.1 transforme les accents en caractères illisibles.
Une application, un périmètre et un certificat
Côté tenant de l’expéditeur, l’application n’a pas de permission API globale consentie dans Entra pour cet envoi : le rôle Application Mail.Send et sa portée sont accordés dans Exchange. Les valeurs ci-dessous sont des exemples à remplacer dans une session Exchange Online disposant des droits nécessaires :
New-ServicePrincipal -AppId <AppId> -ObjectId <ObjectId_du_service_principal> -DisplayName "WSUS-Rapports"
New-ManagementScope -Name "Scope-WSUS-Rapports" -RecipientRestrictionFilter "PrimarySmtpAddress -eq 'rapports@example.com'"
New-ManagementRoleAssignment -App <AppId> -Role "Application Mail.Send" -CustomResourceScope "Scope-WSUS-Rapports"
Test-ServicePrincipalAuthorization -Identity <AppId> -Resource rapports@example.com
L’ObjectId attendu est celui du service principal dans les applications d’entreprise, et non l’identifiant de l’objet « inscription d’application ». Il faut aussi vérifier l’absence d’un droit Mail.Send global parallèle dans Entra : les droits Entra et RBAC s’additionnent. Le test doit autoriser la boîte prévue et refuser une boîte hors périmètre. Les changements peuvent nécessiter un délai de propagation.
Sur le serveur WSUS du client, je génère un certificat auto-signé dans le magasin machine, avec une clé privée non exportable. La tâche tourne en SYSTEM et doit pouvoir accéder à cette clé. Seule la partie publique du certificat est ajoutée à l’application. Le script utilise le certificat pour s’authentifier puis appelle sendMail :
$cert = Get-Item "Cert:\LocalMachine\My\$($cfg.Thumbprint)"
Connect-MgGraph -ClientId $cfg.ClientId -TenantId $cfg.TenantId -Certificate $cert -NoWelcome
Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$($cfg.BoiteEnvoi)/sendMail" -Body $json -ContentType 'application/json'
Le mail reprend la synthèse en HTML, avec le CSV détaillé en pièce jointe. Dans cette implémentation, les caractères non ASCII du JSON sont échappés en \uXXXX pour éviter les soucis rencontrés avec la chaîne d’encodage. Ce n’est pas une obligation de Microsoft Graph : un JSON correctement encodé en UTF-8 est accepté.
Une réponse 202 Accepted de sendMail confirme l’acceptation de la demande, pas la livraison finale. Le suivi de la tâche et des échecs d’envoi reste nécessaire.
Le dernier jeudi du mois
La cmdlet New-ScheduledTaskTrigger n’expose pas directement un déclencheur mensuel. schtasks.exe permet de définir le dernier jeudi du mois à 8 h, selon l’heure du serveur :
schtasks /Create /TN "Rapport WSUS mensuel" /SC MONTHLY /MO LAST /D THU /ST 08:00 /RU SYSTEM /RL HIGHEST /TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\RapportWsus\Export-EtatParcWsus.ps1 -Envoyer"
Le script n'envoie un mail qu'avec le switch -Envoyer. Je peux donc le lancer à la main pour vérifier un chiffre sans spammer le client à chaque essai.
Pour installer le tout, j'ai écrit trois scripts relançables. Le premier prépare le tenant une fois pour toutes : boîte partagée, service principal, scope et rôle. Le deuxième installe le rapport sur chaque serveur client : module Graph, certificat, ajout du certificat à l'app, config et tâche planifiée. Le troisième est le rapport lui-même.
Ce que j’en retiens
Un rapport de conformité qui affiche 99 % n'est pas forcément un bon rapport. C'est souvent un rapport qui compte les MAJ non applicables, ignore les machines muettes et lit une propriété qui n'existe pas. Avant de faire confiance à un chiffre WSUS, je regarde maintenant sa formule et les deux dates de chaque machine.
Côté envoi, le trio application sans permission globale d’envoi dans Entra, rôle Exchange limité à une boîte et certificat non exportable par serveur peut servir à d’autres rapports. Il évite le mot de passe stocké et le relais SMTP dans cette solution.
Une application partagée par plusieurs serveurs reste une seule identité avec les mêmes droits : un certificat différent par serveur permet de renouveler ou retirer cet accès séparément, mais n’isole pas les autorisations entre clients. Une clé non exportable reste utilisable par une identité autorisée sur le serveur. Il faut donc prévoir les accès à la clé, l’expiration et la rotation des certificats.
Il reste un point en suspens : les 46 machines fantômes, muettes depuis plus de 180 jours, attendent toujours qu'on lance le nettoyage. Le rapport, au moins, les affiche maintenant chaque mois.