12 recettes utiles, pas un audit complet
Une licence visible n’explique pas toujours comment elle est arrivée sur un compte ni pourquoi un service refuse de démarrer. Ces 12 recettes PowerShell relient utilisateurs, SKU, plans de service et groupes. Les dix premières lisent des données; les deux dernières illustrent des changements individuels protégés par un interrupteur désactivé.
Exécutez-les dans l’ordre dans une session dédiée. Tous les identifiants sont fictifs. Ce diagnostic ciblé ne mesure ni l’usage réel des services ni l’ensemble de la sécurité Microsoft 365 et ne remplace pas une évaluation complète.
Préparer PowerShell, l’identité et les permissions
Utilisez les modules stables Microsoft Graph avec les API v1.0, pas les anciens modules AzureAD/MSOnline. PowerShell 7 est recommandé. Installez les sous-modules dans une étape approuvée, figez les versions validées et consignez celles chargées; une mise à jour automatique n’est pas un prérequis de collecte.
Le scénario est interactif et délégué : compte administratif nominatif, MFA et bon locataire. Les scopes OAuth, le consentement administrateur et les rôles Entra sont des contrôles distincts. Commencez par les lectures nécessaires. Directory Readers est notamment pris en charge pour les lectures de SKU et de détails de licence; certaines opérations demandent des rôles spécifiques.
# PowerShell 7; approved, tested module versions
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Users.Actions
Import-Module Microsoft.Graph.Groups
Import-Module Microsoft.Graph.Identity.DirectoryManagement
Get-Module Microsoft.Graph.* | Select-Object Name,Version
$tenantId = '33333333-3333-3333-3333-333333333333' # fictional tenant
Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.Read.All' -ContextScope Process
Get-MgContext | Select-Object TenantId,Account,AuthType,Scopes
# Only for checks 8–10, reconnect with approved group read scopes:
# Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.Read.All','Group.Read.All','GroupMember.Read.All' -ContextScope Process1. Inventorier les utilisateurs avec les propriétés utiles
Permission déléguée : User.Read.All.
-All parcourt les pages; -Property demande les champs non retournés par défaut. Un locataire volumineux exige une collecte planifiée et des reprises sur limitation de débit. L’inventaire est un instantané, pas un journal d’activité.
$users = @(Get-MgUser -All -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,UserType,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop)
$users | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,UserType2. Examiner un compte pilote précis
Permission déléguée : User.Read.All.
Privilégiez l’Object ID pour éviter les ambiguïtés après renommage. Confirmez l’UPN et le propriétaire avant un changement. Une propriété vide non demandée avec -Property ne prouve pas qu’elle est vide dans Entra.
$userId = '11111111-1111-1111-1111-111111111111' # fictional OBJECT ID
$user = Get-MgUser -UserId $userId -Property Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
$user | Select-Object Id,DisplayName,UserPrincipalName,AccountEnabled,UsageLocation,AssignedLicenses3. Repérer les comptes désactivés encore licenciés
Permission déléguée : User.Read.All.
Cette sélection locale réutilise la collecte 1. Un compte désactivé peut conserver une licence pour des données, une conservation ou un transfert métier. Faites valider le besoin; ne retirez rien à partir de ce filtre seul.
$users | Where-Object { $_.AccountEnabled -eq $false -and @($_.AssignedLicenses).Count -gt 0 } |
Select-Object Id,UserPrincipalName,UsageLocation,
@{Name='SkuIds';Expression={($_.AssignedLicenses.SkuId -join ';')}}4. Lister les SKU souscrits et leurs compteurs
Permission déléguée : LicenseAssignment.Read.All.
Utilisez SkuId pour les opérations et SkuPartNumber pour le repérage; les noms commerciaux se vérifient dans la table Microsoft. Les compteurs ne remplacent pas les conditions contractuelles ni une réservation de capacité. Conservez le statut de la souscription.
$skus = @(Get-MgSubscribedSku -All -ErrorAction Stop)
$skus | Select-Object SkuId,SkuPartNumber,CapabilityStatus,ConsumedUnits,
@{Name='EnabledUnits';Expression={$_.PrepaidUnits.Enabled}}5. Lire les détails des licences et plans d’un utilisateur
Permission déléguée : LicenseAssignment.Read.All.
Inspectez ProvisioningStatus de chaque plan. Une licence présente n’implique pas que chaque service soit disponible. Cet endpoint est utilisé ici en mode délégué; ne transposez pas sans validation à une identité applicative. Le rôle Directory Readers est l’un des rôles pris en charge pour cette lecture.
Get-MgUserLicenseDetail -UserId $userId -All -ErrorAction Stop |
Select-Object SkuId,SkuPartNumber,ServicePlans |
ConvertTo-Json -Depth 106. Distinguer attribution directe et groupe
Permission déléguée : User.Read.All.
AssignedByGroup nul indique une attribution directe; sinon, résolvez l’ID du groupe. Un même SKU peut avoir plusieurs chemins d’attribution. AssignedLicenses seul ne suffit pas à identifier la source ou la politique qui doit être corrigée.
$user.LicenseAssignmentStates |
Select-Object SkuId,AssignedByGroup,State,Error,DisabledPlans,LastUpdatedDateTime7. Isoler les erreurs d’attribution
Permission déléguée : User.Read.All.
Corrélez état et code : capacité, dépendance de plan, offre incompatible ou usageLocation peuvent être en cause. Ne relancez pas en boucle une attribution sans corriger la cause. L’absence d’erreur dans un instantané ne prouve pas la disponibilité de l’application.
foreach ($u in $users) {
foreach ($a in $u.LicenseAssignmentStates) {
if ($a.State -in @('Error','ActiveWithError') -or
($a.Error -and $a.Error -ne 'None')) {
[pscustomobject]@{
UserId=$u.Id; UPN=$u.UserPrincipalName; SkuId=$a.SkuId
GroupId=$a.AssignedByGroup; State=$a.State; Error=$a.Error
}
}
}
}8. Inventorier les groupes portant des licences
Permission déléguée : Group.Read.All.
Le groupe est parfois la bonne unité de remédiation. Une modification peut toucher tous ses membres. Vérifiez règles dynamiques, plans désactivés et licences exigées pour l’attribution par groupe et les groupes dynamiques; elles ne se déduisent pas de la seule réponse Graph.
$groups = @(Get-MgGroup -All -Property Id,DisplayName,GroupTypes,AssignedLicenses,MembershipRule,MembershipRuleProcessingState -ErrorAction Stop)
$groups | Where-Object { @($_.AssignedLicenses).Count -gt 0 } |
Select-Object Id,DisplayName,GroupTypes,AssignedLicenses,MembershipRule,MembershipRuleProcessingState |
ConvertTo-Json -Depth 109. Lister les membres directs d’un groupe pilote
Permission déléguée : GroupMember.Read.All.
Le résultat peut contenir plusieurs types d’objets. Il s’agit de membres directs; ne supposez pas une expansion transitive ou l’héritage des licences via groupes imbriqués. Les appartenances masquées exigent des permissions supplémentaires et une justification.
$groupId = '22222222-2222-2222-2222-222222222222' # fictional group ID
Get-MgGroupMember -GroupId $groupId -All -ErrorAction Stop |
Select-Object Id,@{Name='Type';Expression={$_.AdditionalProperties['@odata.type']}}10. Lister les appartenances directes du compte pilote
Permission déléguée : User.Read.All.
Ces objets peuvent inclure groupes et rôles d’annuaire. Résolvez uniquement les ID utiles avec la permission adaptée; des détails limités ne signifient pas une absence d’objet. Comparez les groupes aux sources du contrôle 6 avant de retirer une attribution directe.
Get-MgUserMemberOf -UserId $userId -All -ErrorAction Stop |
Select-Object Id,@{Name='Type';Expression={$_.AdditionalProperties['@odata.type']}}Avant les changements : réduire le rayon d’impact
Obtenez l’accord du propriétaire métier, confirmez les droits de licence et choisissez un utilisateur pilote non privilégié. Exportez dans un emplacement restreint l’Object ID, la localisation, les SKU, DisabledPlans et chaque source d’attribution. Les exports contiennent des données personnelles : accès limité et durée de conservation définie.
La lecture de Graph ne crée pas un droit d’usage produit. Vérifiez les conditions des offres et des licences par groupe, particulièrement les groupes dynamiques. Les licences héritées sont traitées de façon asynchrone; les groupes imbriqués ne distribuent pas leurs licences aux membres indirects. N’élargissez pas les permissions pour contourner une erreur 403.
11. Corriger UsageLocation sur le pilote
Changement explicite : User.ReadWrite.All et un rôle autorisé à modifier cet utilisateur, par exemple User Administrator pour ce pilote non administrateur. UsageLocation est un code ISO à deux lettres, pas une adresse ni une préférence linguistique. Confirmez le pays réellement applicable; CA est seulement un exemple.
L’exemple refuse une valeur initialement absente : la propriété n’est pas nullable, donc remettre null n’est pas un retour arrière valide. La variable false fait uniquement afficher la proposition. Préservez la valeur précédente hors session avant d’autoriser l’écriture.
# CHANGE: existing location correction on a non-admin pilot
# Separate approved write session; refresh check 2 first
Connect-MgGraph -TenantId $tenantId -Scopes 'User.ReadWrite.All' -ContextScope Process
$before = Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($before.UsageLocation)) {
throw 'Missing previous location: define an initial-population recovery plan first.'
}
$previousLocation = $before.UsageLocation
$applyLocation = $false
[pscustomobject]@{User=$before.UserPrincipalName; Before=$previousLocation; After='CA'}
if ($applyLocation) {
Update-MgUser -UserId $userId -UsageLocation 'CA' -ErrorAction Stop
Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation -ErrorAction Stop
}
# Approved rollback: Update-MgUser -UserId $userId -UsageLocation $previousLocation12. Ajouter une nouvelle licence directe au pilote
Changement explicite : LicenseAssignment.ReadWrite.All, plus User.Read.All pour la relecture, et rôle License Administrator autorisé. Choisissez un vrai SkuId de la collecte 4, vérifiez capacité, dépendances, pays et besoin métier. Le garde-fou refuse un SKU déjà présent, quelle que soit sa source.
DisabledPlans vide active tous les plans disponibles de cette attribution; faites approuver la sélection avant de passer le commutateur à true. RemoveLicenses vide ne retire aucune licence. Un succès HTTP ne garantit pas la fin du provisionnement : relisez 5, 6 et 7 et testez le service attendu.
# CHANGE: new direct assignment, never a bulk removal
Connect-MgGraph -TenantId $tenantId -Scopes 'User.Read.All','LicenseAssignment.ReadWrite.All' -ContextScope Process
$skuId = [guid]'44444444-4444-4444-4444-444444444444' # replace from check 4
$baseline = Get-MgUser -UserId $userId -Property Id,UserPrincipalName,UsageLocation,AssignedLicenses,LicenseAssignmentStates -ErrorAction Stop
if ([string]::IsNullOrWhiteSpace($baseline.UsageLocation)) { throw 'Confirm usage location first.' }
if (@($baseline.AssignedLicenses | Where-Object { $_.SkuId -eq $skuId }).Count -gt 0) {
throw 'SKU already assigned: inspect source and plans; do not overwrite.'
}
$applyLicense = $false
$approvedDisabledPlans = @() # explicitly enables all available plans: review first
[pscustomobject]@{User=$baseline.UserPrincipalName; SkuId=$skuId; DisabledPlans=$approvedDisabledPlans}
if ($applyLicense) {
Set-MgUserLicense -UserId $userId -AddLicenses @(@{
SkuId=$skuId; DisabledPlans=$approvedDisabledPlans
}) -RemoveLicenses @() -ErrorAction Stop
}Valider, dépanner et revenir en arrière
- Recollectez le pilote après le délai de traitement. Comparez les sources et les plans au relevé initial, puis validez ouverture de session et service avec son propriétaire.
- 403 : distinguez consentement, scope, rôle et périmètre administratif. 404 : validez Object ID et locataire. 429 : respectez Retry-After et espacez les collectes; ne lancez pas une boucle agressive.
- Pour une erreur de licence, examinez le détail : capacité, dépendances de plans et disponibilité géographique. Consignez résultat et heure opérationnelle sans exposer les données dans un ticket public.
- Retour arrière localisation : restaurez la valeur enregistrée si elle reste correcte. Retour arrière licence : seule la nouvelle attribution directe créée par ce pilote peut être envisagée pour retrait, après validation métier et conservation des données. Aucun retrait automatique n’est fourni.
- Une attribution héritée se gère à sa source; changer un groupe peut toucher tous ses membres. Réattribuer une licence ne restaure pas des données déjà supprimées.
- Terminez par Disconnect-MgGraph. Cela ferme la session, sans révoquer le consentement OAuth déjà accordé. Révisez séparément les autorisations inutiles.
Passer d’une collecte à un plan d’action
Un résultat utile tient en quatre colonnes : observation, impact, propriétaire, prochaine validation. Par exemple, une erreur de capacité sur un pilote devient une vérification de l’offre et du besoin, pas une suppression de licences ailleurs. Pour standardiser les droits, les groupes et le cycle de vie des comptes, un accompagnement Microsoft 365 permet de cadrer les changements au-delà de ces commandes.