Ś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.

Pobierz .ps1

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