- teams: harden Classic Teams removal, protect New Teams, and clean HKU run keys - dotnet: add bootstrapper EXE uninstall support and remove orphaned 3.1.* folders - teams: add MSIX package targeting and version parsing for vulnerable New Teams builds - m365: improve Graph SDK export speed, reuse sessions, and add null-safe handling
109 lines
3.6 KiB
PowerShell
109 lines
3.6 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Exports Microsoft 365 user recertification and licensing report using Microsoft Graph SDK.
|
|
.DESCRIPTION
|
|
Retrieves all M365 users, resolves license SKU IDs to readable part numbers,
|
|
extracts last interactive sign-in dates, and exports the dataset to CSV.
|
|
.EXAMPLE
|
|
.\Export-M365UserCertification.ps1 -OutputPath ".\M365-User-Certification-Export.csv"
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$OutputPath = ".\M365-User-Certification-Export.csv"
|
|
)
|
|
|
|
# Silence Graph cmdlet progress bars to drastically decrease execution time
|
|
$OriginalProgressPreference = $ProgressPreference
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
try {
|
|
# Check existing Microsoft Graph session
|
|
$CurrentContext = Get-MgContext
|
|
$RequiredScopes = @("User.Read.All", "Directory.Read.All", "AuditLog.Read.All")
|
|
|
|
if (-not $CurrentContext) {
|
|
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
|
|
Connect-MgGraph -Scopes $RequiredScopes
|
|
}
|
|
else {
|
|
Write-Host "Connected as $($CurrentContext.Account)" -ForegroundColor Green
|
|
}
|
|
|
|
# Resolve relative path to absolute path
|
|
$ResolvedOutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
|
|
|
|
Write-Host "Building license SKU lookup table..." -ForegroundColor Cyan
|
|
$SkuLookup = @{}
|
|
Get-MgSubscribedSku -All | ForEach-Object {
|
|
$SkuLookup[$_.SkuId.ToString()] = $_.SkuPartNumber
|
|
}
|
|
|
|
# Explicitly define target properties (signInActivity requires AuditLog.Read.All)
|
|
$Properties = @(
|
|
"id",
|
|
"displayName",
|
|
"userPrincipalName",
|
|
"userType",
|
|
"accountEnabled",
|
|
"assignedLicenses",
|
|
"externalUserState",
|
|
"signInActivity"
|
|
)
|
|
|
|
Write-Host "Fetching user data from Microsoft Graph..." -ForegroundColor Cyan
|
|
$Users = Get-MgUser -All -Property $Properties
|
|
|
|
Write-Host "Processing $($Users.Count) users..." -ForegroundColor Cyan
|
|
|
|
$Export = foreach ($User in $Users) {
|
|
# Resolve licenses
|
|
$AssignedLicenses = if ($User.AssignedLicenses -and $User.AssignedLicenses.Count -gt 0) {
|
|
($User.AssignedLicenses | ForEach-Object {
|
|
$SkuId = $_.SkuId.ToString()
|
|
if ($SkuLookup.ContainsKey($SkuId)) {
|
|
$SkuLookup[$SkuId]
|
|
}
|
|
else {
|
|
$SkuId
|
|
}
|
|
}) -join "; "
|
|
}
|
|
else {
|
|
"None"
|
|
}
|
|
|
|
# Safe null-check for SignInActivity
|
|
$LastSignIn = if ($User.SignInActivity -and $User.SignInActivity.LastSignInDateTime) {
|
|
$User.SignInActivity.LastSignInDateTime
|
|
}
|
|
else {
|
|
"Never / No Record"
|
|
}
|
|
|
|
[PSCustomObject]@{
|
|
Name = $User.DisplayName
|
|
UserPrincipalName = $User.UserPrincipalName
|
|
UserType = $User.UserType
|
|
AccountEnabled = $User.AccountEnabled
|
|
AssignedLicenses = $AssignedLicenses
|
|
GuestInvitationState = $User.ExternalUserState
|
|
LastInteractiveSignInDate = $LastSignIn
|
|
}
|
|
}
|
|
|
|
# Export results to CSV
|
|
$Export |
|
|
Sort-Object Name |
|
|
Export-Csv -Path $ResolvedOutputPath -NoTypeInformation -Encoding UTF8
|
|
|
|
Write-Host "Export complete: $ResolvedOutputPath" -ForegroundColor Green
|
|
|
|
}
|
|
catch {
|
|
Write-Error "An error occurred during execution: $_"
|
|
}
|
|
finally {
|
|
# Restore original progress preference
|
|
$ProgressPreference = $OriginalProgressPreference
|
|
} |