Skip to content
  • There are no suggestions because the search field is empty.

EntraID / MS365 Device Check Script

What devices have logged in within the last 30-days, their OS, and users, so we can use for our CE/CE+

We want to determine who and what has authenticated into EntraID within the last 30-days, and what the OS was and who the user was; therefore, this PowerShell script, run in admin mode, will authenticate with EntraID, pull the last 30-day logs, work out the devices and, where possible, use Intune to get further details.

Download the code, save, and execute: .\ms365-device-check.ps1

You can use the script with filters:

  • Windows and macOS only: .\ms365-device-check.ps1 -OSFilter @("Windows","macOS")
  • Mobile devices only: .\ms365-device-check.ps1 -OSFilter @("Android","iOS","iPadOS")
  • Change the timeline: .\ms365-device-check.ps1 -DaysBack 14
  • Enable a Grid View: .\ms365-device-check.ps1 -Grid
  • Change the export path: .\ms365-device-check.ps1 -ExportCsvPath "C:\Reports\DeviceReport.csv"
  • Use multiple options: .\ms365-device-check.ps1 -DaysBack 30 -OSFilter @("Windows","macOS") -Grid -ExportCsvPath "C:\Reports\Devices.csv"

NOTE: The OS filtering is string contains matching, not strict equality:

  • Windows 11 will match "Windows"

  • macOS 14 will match "macOS"

  • Android 14 will match "Android"

<#
======================================================================
Entra / M365 Devices Active (Last N Days) + Exact OS Version (Intune)
======================================================================

What it does:
1) Ensures the full Microsoft.Graph PowerShell SDK is installed (meta-module)
2) Interactive GUI sign-in (standard Microsoft login prompt)
3) Pulls Entra Sign-in logs for the last N days
4) For each device, finds the MOST RECENT sign-in in the window and captures:
   - LastSignInTime
   - LastSignInUser (UPN + Display Name)
5) Enriches each device using Intune managed device inventory to get:
   - Exact OS Version (osVersion) for iOS/Android/Windows/macOS (when managed)
   - More accurate Windows 10 vs 11 (inferred from build >= 22000 => Windows 11)
6) Optional OS filtering
7) Mobile devices sorted to the end
8) Outputs to console + optional Out-GridView + optional CSV

Required delegated Graph scopes:
- AuditLog.Read.All
- DeviceManagementManagedDevices.Read.All
- Directory.Read.All (often needed in combination in some tenants)

Important limitations:
- Exact OS version/build is typically ONLY reliable for Intune-managed devices.
- Entra sign-in logs alone often only show “Android” or “Windows 10” and not exact builds.
- Sign-in log retention depends on licensing; the script can only query what exists.

============================================================
#>

param(
    [int]$DaysBack = 30,

    # Optional OS filter tokens (matches against FinalOS, e.g. "Windows 11", "iOS", "Android", "macOS")
    [string[]]$OSFilter = @(),

    # Show Out-GridView (where supported)
    [switch]$Grid,

    # Set to "" to skip CSV export
    [string]$ExportCsvPath = ".\Entra_Devices_LastSignIn_Enriched.csv"
)

Clear-Host
$ErrorActionPreference = "Stop"

Write-Host "Starting Entra Devices report (Last Sign-In + OS Version via Intune)..." -ForegroundColor Cyan
Write-Host ""

# --- Ensure TLS 1.2 (older hosts) ---
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}

# --- Ensure NuGet provider ---
if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
    Write-Host "Installing NuGet package provider..." -ForegroundColor Yellow
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Scope CurrentUser -Force | Out-Null
}

# --- Ensure Microsoft Graph PowerShell SDK (meta-module) is installed ---
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph)) {
    Write-Host "Installing full Microsoft.Graph PowerShell SDK (this can be large)..." -ForegroundColor Yellow
    Install-Module Microsoft.Graph -Scope CurrentUser -AllowClobber -Force
}

# NOTE: We deliberately do NOT Import-Module individual submodules.
# The meta-module + PowerShell module autoloading will load the needed cmdlets.
try {
    Import-Module Microsoft.Graph -ErrorAction Stop
} catch {
    # If import fails for any reason, we still might succeed via autoloading,
    # but usually Import-Module Microsoft.Graph works once installed.
    Write-Host "Warning: Could not Import-Module Microsoft.Graph. Attempting to continue with autoloading..." -ForegroundColor Yellow
}

# --- Connect (GUI login) ---
$scopes = @(
    "AuditLog.Read.All",
    "DeviceManagementManagedDevices.Read.All",
    "Directory.Read.All"
)

Write-Host "Connecting to Microsoft Graph (GUI sign-in will appear)..." -ForegroundColor Cyan
Connect-MgGraph -Scopes $scopes | Out-Null

# Optional: show context
$ctx = Get-MgContext
Write-Host ("Connected as: {0}" -f $ctx.Account) -ForegroundColor DarkCyan
Write-Host ""

# --- Time window ---
$since = (Get-Date).ToUniversalTime().AddDays(-1 * [double]$DaysBack)
$sinceIso = $since.ToString("yyyy-MM-ddTHH:mm:ssZ")

# --- Helper: Extract deviceDetail fields safely across SDK shapes ---
function Get-DeviceDetailField {
    param(
        [Parameter(Mandatory=$true)]$SignIn,
        [Parameter(Mandatory=$true)][string]$FieldName
    )

    # Standard shape: $SignIn.DeviceDetail.<Field>
    try {
        $dd = $SignIn.DeviceDetail
        if ($null -ne $dd) {
            $v = $dd.$FieldName
            if ($null -ne $v -and "$v".Trim().Length -gt 0) { return $v }
        }
    } catch {}

    # Alternate shape: AdditionalProperties["deviceDetail"][field]
    try {
        if ($SignIn.AdditionalProperties -and $SignIn.AdditionalProperties.ContainsKey("deviceDetail")) {
            $dd2 = $SignIn.AdditionalProperties["deviceDetail"]
            if ($dd2 -and $dd2.ContainsKey($FieldName)) {
                $v2 = $dd2[$FieldName]
                if ($null -ne $v2 -and "$v2".Trim().Length -gt 0) { return $v2 }
            }
        }
    } catch {}

    return $null
}

function Get-WindowsFriendlyName {
    param([string]$OsVersion)

    # Intune often returns 10.0.BUILD.REV e.g. 10.0.22631.3007
    if ([string]::IsNullOrWhiteSpace($OsVersion)) { return $null }

    $parts = $OsVersion.Split(".")
    if ($parts.Count -ge 3) {
        $buildStr = $parts[2]
        [int]$build = 0
        if ([int]::TryParse($buildStr, [ref]$build)) {
            if ($build -ge 22000) { return "Windows 11" }
            return "Windows 10"
        }
    }
    return $null
}

function Is-MobileOS {
    param([string]$FinalOS)
    if ([string]::IsNullOrWhiteSpace($FinalOS)) { return $false }
    return ($FinalOS -match 'Android|iOS|iPadOS|Windows Phone')
}

Write-Host "Pulling Entra sign-in logs since $sinceIso ..." -ForegroundColor Cyan

# Successful sign-ins only (reduces noise and volume)
$filter = "createdDateTime ge $sinceIso and status/errorCode eq 0"

# Pull ALL sign-ins in window (can be heavy on large tenants)
$signIns = Get-MgAuditLogSignIn -Filter $filter -All

if (-not $signIns -or $signIns.Count -eq 0) {
    Write-Host ""
    Write-Host "No sign-in events returned (retention/permissions/empty window)." -ForegroundColor Yellow
    return
}

# --- Build latest sign-in per deviceId (deviceId is the join key for Intune enrichment) ---
$signInsWithDevice = foreach ($s in $signIns) {
    $deviceId = Get-DeviceDetailField -SignIn $s -FieldName "deviceId"
    if ([string]::IsNullOrWhiteSpace($deviceId)) { continue }

    [pscustomobject]@{
        DeviceId        = $deviceId
        CreatedDateTime = $s.CreatedDateTime
        Raw             = $s
    }
}

if (-not $signInsWithDevice -or $signInsWithDevice.Count -eq 0) {
    Write-Host ""
    Write-Host "Sign-ins returned, but none had deviceId in deviceDetail, so per-device grouping isn't possible." -ForegroundColor Yellow
    return
}

$latestSignInPerDevice =
    $signInsWithDevice |
    Sort-Object CreatedDateTime -Descending |
    Group-Object DeviceId |
    ForEach-Object { $_.Group[0].Raw }

# Create a set of Azure AD deviceIds needed for enrichment
$deviceIdSet = @{}
foreach ($s in $latestSignInPerDevice) {
    $did = Get-DeviceDetailField -SignIn $s -FieldName "deviceId"
    if (-not [string]::IsNullOrWhiteSpace($did)) { $deviceIdSet[$did] = $true }
}

Write-Host ""
Write-Host "Pulling Intune managed devices for OS version/build enrichment..." -ForegroundColor Cyan

# Pull managed devices inventory
# NOTE: If this is too heavy in very large tenants, you can filter by lastSyncDateTime ge $sinceIso,
# but that may miss devices that signed in but haven't synced recently.
$managedDevices = Get-MgDeviceManagementManagedDevice -All -Property `
    "id,deviceName,operatingSystem,osVersion,azureADDeviceId,userPrincipalName,lastSyncDateTime,model,manufacturer"

# Build lookup by Azure AD Device ID (GUID string)
$intuneByAzureAdDeviceId = @{}
foreach ($m in $managedDevices) {
    if (-not [string]::IsNullOrWhiteSpace($m.AzureAdDeviceId)) {
        $intuneByAzureAdDeviceId[$m.AzureAdDeviceId] = $m
    }
}

Write-Host ""
Write-Host "Building report..." -ForegroundColor Cyan

$report = foreach ($s in $latestSignInPerDevice) {

    # Sign-in derived values
    $deviceId          = Get-DeviceDetailField -SignIn $s -FieldName "deviceId"
    $signInDeviceName  = Get-DeviceDetailField -SignIn $s -FieldName "displayName"
    $signInOS          = Get-DeviceDetailField -SignIn $s -FieldName "operatingSystem"

    $lastSignInTimeUtc  = $s.CreatedDateTime
    $lastSignInUserUPN  = $s.UserPrincipalName
    $lastSignInUserName = $s.UserDisplayName

    # Defaults
    $finalDeviceName = $signInDeviceName
    if ([string]::IsNullOrWhiteSpace($finalDeviceName)) { $finalDeviceName = "(Unknown device name)" }

    $finalOS = $signInOS
    if ([string]::IsNullOrWhiteSpace($finalOS)) { $finalOS = "(Unknown OS)" }

    # Intune enrichment values
    $osVersion     = $null
    $intuneUserUPN = $null
    $intuneLastSync= $null
    $manufacturer  = $null
    $model         = $null
    $isManagedInIntune = $false

    if (-not [string]::IsNullOrWhiteSpace($deviceId) -and $intuneByAzureAdDeviceId.ContainsKey($deviceId)) {
        $m = $intuneByAzureAdDeviceId[$deviceId]
        $isManagedInIntune = $true

        if (-not [string]::IsNullOrWhiteSpace($m.DeviceName))       { $finalDeviceName = $m.DeviceName }
        if (-not [string]::IsNullOrWhiteSpace($m.OperatingSystem))  { $finalOS         = $m.OperatingSystem }
        if (-not [string]::IsNullOrWhiteSpace($m.OsVersion))        { $osVersion       = $m.OsVersion }

        $intuneUserUPN = $m.UserPrincipalName
        $intuneLastSync= $m.LastSyncDateTime
        $manufacturer  = $m.Manufacturer
        $model         = $m.Model
    }

    # Improve Windows label using build, where possible
    # (Entra sign-in logs often say "Windows 10"; Intune osVersion/build lets us distinguish)
    if ($finalOS -match '^Windows$|^Windows 10$|^Windows10$|^Windows 11$|^Windows11$') {
        $friendly = Get-WindowsFriendlyName -OsVersion $osVersion
        if (-not [string]::IsNullOrWhiteSpace($friendly)) { $finalOS = $friendly }
    }

    $mobile = Is-MobileOS -FinalOS $finalOS

    [pscustomobject]@{
        DeviceName           = $finalDeviceName
        OS                   = $finalOS
        OSVersion            = $osVersion

        LastSignInTimeUtc    = $lastSignInTimeUtc
        LastSignInUserUPN    = $lastSignInUserUPN
        LastSignInUserName   = $lastSignInUserName

        # Intune context (useful when different from last sign-in user)
        IntunePrimaryUserUPN = $intuneUserUPN
        IntuneLastSyncUtc    = $intuneLastSync
        IntuneManaged        = $isManagedInIntune

        Manufacturer         = $manufacturer
        Model                = $model

        MobileDevice         = [bool]$mobile
    }
}

# Optional OS filtering
if ($OSFilter -and $OSFilter.Count -gt 0) {
    $report = $report | Where-Object {
        foreach ($f in $OSFilter) {
            if ($_.OS -like "*$f*") { return $true }
        }
        return $false
    }
}

# Sort with mobile at the end
$report = $report | Sort-Object MobileDevice, OS, DeviceName

Write-Host ""
Write-Host "===== ENTRA DEVICES: LAST SIGN-IN + OS VERSION (Last $DaysBack Days) =====" -ForegroundColor Green

$report |
    Select-Object DeviceName, OS, OSVersion, LastSignInTimeUtc, LastSignInUserUPN, IntunePrimaryUserUPN, IntuneManaged, MobileDevice |
    Format-Table -AutoSize

if ($Grid) {
    try {
        $report | Out-GridView -Title "Entra Devices - Last Sign-In + OS Version (Last $DaysBack Days)"
    } catch {
        Write-Host "Out-GridView not available in this host." -ForegroundColor Yellow
    }
}

if (-not [string]::IsNullOrWhiteSpace($ExportCsvPath)) {
    $report | Export-Csv $ExportCsvPath -NoTypeInformation -Encoding UTF8
    Write-Host ""
    Write-Host "Exported CSV: $ExportCsvPath" -ForegroundColor Cyan
}

Write-Host ""
Write-Host "Done." -ForegroundColor Cyan