Śledzi użytkowników posiadających licencję M365 i zapisuje historię w CSV.
✉️ Exchange Online POWERSHELL ChrisTitusTechŚledzi użytkowników posiadających licencję M365 i zapisuje historię w CSV.
Opis
Nowoczesna wersja Get-NewOfficeUsers.ps1 — zastępuje MSOL (EOL 2024) Microsoft Graph SDK. MIGRACJA z MSOL: Get-MsolUser → Get-MgUser -Filter "assignedLicenses/$count ne 0" Get-MsolUserLicense → (Get-MgUser).AssignedLicenses + Get-MgSubscribedSku Connect-MsolService → Connect-MgGraph -Scopes User.Read.All Funkcje: - Update-M365LicenseTracking — aktualizuje CSV (dodaje nowych, oznacza odlicencjonowanych) - Get-RecentlyLicensedUsers — zwraca użytkowników licencjonowanych po określonej dacie
📄 Track-M365LicensedUsers-Modern.ps1
🕒 2026-04-13
📦 Źródło: christitustech
Track-M365LicensedUsers-Modern.ps1
#Requires -Modules Microsoft.Graph.Users, Microsoft.Graph.Identity.DirectoryManagement
#Requires -Version 7.0
<#
.SYNOPSIS
Śledzi użytkowników posiadających licencję M365 i zapisuje historię w CSV.
.DESCRIPTION
Nowoczesna wersja Get-NewOfficeUsers.ps1 — zastępuje MSOL (EOL 2024) Microsoft Graph SDK.
MIGRACJA z MSOL:
Get-MsolUser → Get-MgUser -Filter "assignedLicenses/$count ne 0"
Get-MsolUserLicense → (Get-MgUser).AssignedLicenses + Get-MgSubscribedSku
Connect-MsolService → Connect-MgGraph -Scopes User.Read.All
Funkcje:
- Update-M365LicenseTracking — aktualizuje CSV (dodaje nowych, oznacza odlicencjonowanych)
- Get-RecentlyLicensedUsers — zwraca użytkowników licencjonowanych po określonej dacie
.PARAMETER ServicePlanName
Nazwa Service Planu do śledzenia (np. "OFFICESUBSCRIPTION", "TEAMS1", "EXCHANGE_S_STANDARD")
Listę dostępnych planów: Get-MgSubscribedSku | Select -ExpandProperty ServicePlans | Select ServicePlanName
.PARAMETER CSVPath
Ścieżka do pliku CSV z historią licencji.
.PARAMETER CutOffDate
Data graniczna dla Get-RecentlyLicensedUsers (domyślnie: 7 dni wstecz).
.EXAMPLE
.\Track-M365LicensedUsers-Modern.ps1 -ServicePlanName "OFFICESUBSCRIPTION"
.EXAMPLE
.\Track-M365LicensedUsers-Modern.ps1 -ServicePlanName "TEAMS1" -CutOffDate (Get-Date).AddDays(-30)
.NOTES
Autor: Modernizacja: Senior PS / baza-wiedzy-IT
Wersja: 2.0.0 (2026)
Wymaga: Microsoft.Graph.Users — Install-Module Microsoft.Graph -Scope CurrentUser
WAŻNE: MSOnline (Connect-MsolService) jest EOL od marca 2024.
Wszystkie operacje MSOL muszą być zastąpione przez Microsoft Graph PowerShell SDK.
#>
[CmdletBinding(DefaultParameterSetName = "Track")]
param(
[Parameter(ParameterSetName = "Track")]
[Parameter(ParameterSetName = "Report")]
[ValidateNotNullOrEmpty()]
[string]$ServicePlanName = "OFFICESUBSCRIPTION",
[string]$CSVPath = "$env:APPDATA\Microsoft\OfficeAutomation\M365LicenseTracking.csv",
[Parameter(ParameterSetName = "Report")]
[datetime]$CutOffDate = (Get-Date).AddDays(-7),
[Parameter(ParameterSetName = "Report")]
[switch]$ShowRecentOnly
)
# ============================================================
# POŁĄCZENIE — Microsoft Graph (zastępuje Connect-MsolService)
# ============================================================
$requiredScopes = @(
"User.Read.All", # Odczyt użytkowników i licencji
"Organization.Read.All" # Odczyt SKU subskrypcji
)
Write-Host "Łączenie z Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph -Scopes $requiredScopes -NoWelcome -ErrorAction Stop
Write-Verbose "Połączono: $((Get-MgContext).Account)"
# ============================================================
# FUNKCJA: Pobierz SKU ID dla nazwy Service Planu
# ============================================================
function Get-ServicePlanSkuId {
param([string]$PlanName)
# Filter Left — pobieramy wszystkie SKU i szukamy planu po nazwie
[array]$allSkus = Get-MgSubscribedSku -All -Property "skuId,skuPartNumber,servicePlans"
$matchingSkuIds = foreach ($sku in $allSkus) {
$plan = $sku.ServicePlans | Where-Object { $_.ServicePlanName -eq $PlanName }
if ($plan) { $sku.SkuId }
}
if (-not $matchingSkuIds) {
throw "Nie znaleziono Service Planu '$PlanName'. Dostępne plany: $(
($allSkus | ForEach-Object { $_.ServicePlans.ServicePlanName } | Sort-Object -Unique) -join ', '
)"
}
return $matchingSkuIds
}
# ============================================================
# FUNKCJA: Pobierz licencjonowanych użytkowników przez Graph
# (zastępuje: Get-MsolUser | Where-Object IsLicensed -eq $True)
# ============================================================
function Get-LicensedUsersFromGraph {
param([string]$PlanName)
Write-Host "Pobieranie użytkowników z licencją '$PlanName'..." -ForegroundColor Cyan
# Filter Left — tylko użytkownicy z co najmniej jedną licencją
$userParams = @{
Filter = "assignedLicenses/`$count ne 0 and userType eq 'Member'"
Property = "id,displayName,userPrincipalName,mail,assignedLicenses,accountEnabled"
ConsistencyLevel = "eventual"
CountVariable = "userCount"
All = $true
PageSize = 999
}
Write-Verbose "Pobieranie użytkowników z Graph (Filter Left)..."
[array]$licensedUsers = Get-MgUser @userParams
Write-Verbose "Znaleziono $($licensedUsers.Count) użytkowników z licencjami."
# Pobierz SkuIds dla szukanego planu
$targetSkuIds = Get-ServicePlanSkuId -PlanName $PlanName
# Filtruj po SkuId (zastępuje foreach po ServiceStatus)
$result = $licensedUsers | Where-Object {
$userSkuIds = $_.AssignedLicenses.SkuId
$userSkuIds | Where-Object { $_ -in $targetSkuIds }
} | ForEach-Object {
[PSCustomObject]@{
ObjectId = $_.Id
DisplayName = $_.DisplayName
SignInName = $_.UserPrincipalName
Mail = $_.Mail
Enabled = $_.AccountEnabled
}
}
Write-Host " Użytkowników z '$PlanName': $($result.Count)" -ForegroundColor Green
return $result
}
# ============================================================
# FUNKCJA: Aktualizuj historię CSV
# (zastępuje: Update-UserLicenseData z MSOL)
# ============================================================
function Update-M365LicenseTracking {
param(
[string]$ServicePlanName,
[string]$CSVPath
)
$now = Get-Date -Format "yyyy-MM-dd HH:mm"
# Utwórz folder jeśli nie istnieje
$csvDir = Split-Path -Path $CSVPath -Parent
if (-not (Test-Path $csvDir)) {
New-Item -ItemType Directory -Path $csvDir -Force | Out-Null
}
# Pobierz aktualnie licencjonowanych
[array]$currentUsers = Get-LicensedUsersFromGraph -PlanName $ServicePlanName
if (Test-Path $CSVPath) {
# CSV istnieje — aktualizuj
$importedData = Import-Csv -Path $CSVPath -Encoding UTF8
# Oznacz odlicencjonowanych
foreach ($row in $importedData) {
if ($row.DelicensedAsOf -eq "-") {
$stillLicensed = $currentUsers | Where-Object { $_.ObjectId -eq $row.ObjectId }
if (-not $stillLicensed) {
$row.DelicensedAsOf = $now
Write-Verbose "[$($row.SignInName)] Odlicencjonowany: $now"
}
}
}
# Dodaj nowych użytkowników (nie istniejących w CSV)
$existingIds = $importedData.ObjectId
$newEntries = $currentUsers | Where-Object { $_.ObjectId -notin $existingIds } | ForEach-Object {
$_ | Add-Member -NotePropertyName LicensedAsOf -NotePropertyValue $now -PassThru |
Add-Member -NotePropertyName DelicensedAsOf -NotePropertyValue "-" -PassThru
}
if ($newEntries) {
Write-Host " Nowych użytkowników: $($newEntries.Count)" -ForegroundColor Yellow
}
# Zapisz
($importedData + $newEntries) | Export-Csv -Path $CSVPath -NoTypeInformation -Encoding UTF8 -Delimiter ";"
Write-Host "CSV zaktualizowany: $CSVPath" -ForegroundColor Green
} else {
# Nowy plik CSV
$currentUsers | ForEach-Object {
$_ | Add-Member -NotePropertyName LicensedAsOf -NotePropertyValue $now -PassThru |
Add-Member -NotePropertyName DelicensedAsOf -NotePropertyValue "-" -PassThru
} | Export-Csv -Path $CSVPath -NoTypeInformation -Encoding UTF8 -Delimiter ";"
Write-Host "CSV utworzony: $CSVPath ($($currentUsers.Count) rekordów)" -ForegroundColor Green
}
}
# ============================================================
# FUNKCJA: Pokaż nowo licencjonowanych użytkowników
# (zastępuje: Get-RecentlyLicensedUsers z MSOL)
# ============================================================
function Get-RecentlyLicensedUsers {
param(
[string]$CSVPath,
[datetime]$CutOffDate
)
if (-not (Test-Path $CSVPath)) {
Write-Warning "Brak pliku CSV: $CSVPath. Uruchom najpierw Update-M365LicenseTracking."
return
}
Write-Host "Nowi użytkownicy od: $($CutOffDate.ToString('yyyy-MM-dd HH:mm'))" -ForegroundColor Cyan
Import-Csv -Path $CSVPath -Encoding UTF8 -Delimiter ";" |
Where-Object {
$_.LicensedAsOf -and
[datetime]::TryParse($_.LicensedAsOf, [ref]$null) -and
([datetime]$_.LicensedAsOf) -gt $CutOffDate
} |
Select-Object DisplayName, SignInName, Mail, LicensedAsOf |
Sort-Object LicensedAsOf -Descending |
Format-Table -AutoSize
}
# ============================================================
# MAIN
# ============================================================
if ($ShowRecentOnly) {
Get-RecentlyLicensedUsers -CSVPath $CSVPath -CutOffDate $CutOffDate
} else {
Update-M365LicenseTracking -ServicePlanName $ServicePlanName -CSVPath $CSVPath
if ($ShowRecentOnly -or $CutOffDate -ne (Get-Date).AddDays(-7)) {
Get-RecentlyLicensedUsers -CSVPath $CSVPath -CutOffDate $CutOffDate
}
}
Disconnect-MgGraph
Write-Verbose "Rozłączono z Microsoft Graph."