#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."