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