EntraDeviceReport
0.3.2
Read-only Microsoft Entra ID unmanaged and stale device reporting via Microsoft Graph. Generates CSV evidence for device-exposure assessment findings; performs no writes to any tenant.
Minimum PowerShell version
7.4
Installation Options
Owners
Copyright
(c) Olamide Olaleye. All rights reserved.
Package Details
Author(s)
- Olamide Olaleye
Tags
Entra EntraID AzureAD MicrosoftGraph Graph Device DeviceManagement Intune Security Compliance Audit Reporting ReadOnly
Functions
Connect-EntraDeviceReport Get-EntraUnmanagedDeviceReport
PSEditions
Dependencies
This module has no dependencies.
Release Notes
## [0.3.2] - 2026-09-09
### Added
- `Get-EntraUnmanagedDeviceReport`, the module's single public function. Queries
the Microsoft Graph `/devices` resource, classifies each registered device as
managed or unmanaged, evaluates staleness against a configurable threshold
(`-StaleAfterDays`, default 180), and returns report objects or writes a CSV.
Supports `-Path`, `-IncludeManaged`, `-StaleOnly`, `-PassThru`, and
`ShouldProcess` for the one filesystem side effect.
- Nine private helpers, one responsibility each: `Assert-GraphConnection`,
`Get-EntraDeviceObject`, `Resolve-DeviceManagementState`, `Test-DeviceStale`,
`ConvertTo-DeviceReportRow`, `Export-DeviceReportCsv`, `Get-DevicePropertyValue`,
`ConvertTo-UtcDateTime`, and `Format-Iso8601Utc`.
- Eighteen-column report contract, emitted in a fixed order, covering device
identity, the mutually exclusive `Managed`/`UnManaged` pair, ownership, trust
type, platform, sign-in recency, and hybrid-join derivation.
- Categorised terminating errors with stable error IDs, so a missing scope
(`PermissionDenied`), an expired session (`AuthenticationError`), throttling
(`LimitsExceeded`), and a transport fault (`ConnectionError`) are
distinguishable by an operator rather than surfacing identically.
- Pester v6 unit suite, one test file per function, hermetic and mocked
throughout. Verified by mutation testing: fifteen deliberate defects injected
into the source were each confirmed to turn the suite red.
### Added
- `Connect-EntraDeviceReport`, a second public function that wires authentication
into the module. One parameter set per method, ordered by preference: managed
identity (`-ManagedIdentity`, with optional `-ClientId` for a user-assigned
identity), workload identity federation via a pre-acquired `-AccessToken`,
certificate app-only (`-CertificateThumbprint`, `-CertificateSubjectName`, or
a loaded `-Certificate`), `-EnvironmentVariable`, interactive browser (the
default), and client secret as a last resort. `-Environment` supports
sovereign clouds across every method.
- `-UseDeviceCode` as an **optional** switch on the interactive parameter set
only. It is never mandatory and never required by another method; omitting it
leaves interactive browser sign-in as the default. Using it emits a warning
naming Conditional Access authentication-flows policies and the
Microsoft-managed device code block as the reasons it may fail at sign-in.
- `-TenantId` is **mandatory on every authentication method**. `Connect-MgGraph`
accepts it only on its interactive, certificate and client secret parameter
sets, so for managed identity, access token and environment variable
authentication the module verifies it against the resulting session instead
and throws `GraphTenantMismatch` on a mismatch. Supply a GUID for that check
to run; a domain name is reported as unverifiable on the verbose stream
rather than passing silently.
- `Get-EntraDeviceReportScope`, the single source of truth for the module's
Graph permissions. Both the scope `Connect-EntraDeviceReport` requests and
the scope `Assert-GraphConnection` validates now read from it, so the
least-privilege requirement cannot drift between the two.
- `Assert-GraphTenant`, a private helper that confirms an established session
belongs to the intended tenant.
- `Get-GraphConnectionParameter`, a private, side-effect-free helper that
translates the chosen parameter set into the exact `Connect-MgGraph` argument
set, so dispatch is testable without a network call.
`Get-EntraUnmanagedDeviceReport` still never authenticates. It inspects the
existing session and fails closed, so an unattended report run cannot trigger an
interactive prompt; authentication is `Connect-EntraDeviceReport`'s job alone.
### Fixed
- `Get-EntraUnmanagedDeviceReport` emitted `MethodException: Cannot find an
overload for "Contains" and the argument count: "1"` once per device against a
real tenant. `Get-DevicePropertyValue` called `.Contains($name)` on the
device's `AdditionalProperties`, which the Graph SDK populates with a
`Dictionary[string, object]`. That type's only single-argument `Contains` is
the explicit non-generic `IDictionary` implementation, which PowerShell will
not dispatch to; casting to `[System.Collections.IDictionary]` does not help
either. Now uses `ContainsKey`, which both that type and a plain hashtable
expose.
**Any report produced by 0.2.0 to 0.3.1 against a live tenant should be
re-run.** The open-type fallback threw for every device, so any property not
present on the typed SDK model resolved to `$null`, which can misreport
`ManagementType`, `Ownership`, and therefore the `Managed`/`UnManaged` split.
The tests did not catch this because the fixture built the bag as a PowerShell
hashtable, which *does* have `Contains(object)`. `Get-TestDevice` now builds a
`Dictionary[string, object]` by default, matching the SDK, with
`-AsHashtableBag` retained so both shapes stay covered.
- `-UseDeviceCode` never displayed the authentication URL and code, so the flow
sat silent until it failed with "Authentication timed out after 120 seconds
due to inactivity". `Connect-MgGraph` writes the device code prompt to its
**success stream**, and the wrapper piped that to `Out-Null`. It is now
relayed to the information stream with `-InformationAction Continue`, which
puts the prompt on the console without letting the connection banner leak
into the function's own return value. Verified against the live SDK:
`Connect-MgGraph -UseDeviceCode | Out-Null` emits nothing at all, while the
same call without the pipe prints the code. `-NoWelcome` was ruled out as a
cause; it suppresses only the welcome banner.
- Failure and verbose messages reported the method as `Interactive` during a
device code sign-in, because that is the parameter set name. They now say
`Interactive (device code)`.
- Unit tests now run on a clean machine without the Microsoft Graph SDK
installed. Pester's `Mock` requires the target command to exist, and command
lookup from module scope falls back to global scope, so `tests/TestHelpers.ps1`
defines global stubs for `Get-MgContext`, `Get-MgDevice`, and `Connect-MgGraph`
when the real cmdlets are absent. Without this, every mocked Graph call threw
`CommandNotFoundException` on CI runners while passing on a developer machine.
- Rewrote `tests/QA/repository.tests.ps1`, which had an unbalanced brace that
made Pester discovery fail for the whole file. A discovery failure is not
counted in Pester's `FailedCount`, so the build reported success while an
entire QA file never ran.
### Security
- The module is read-only. It reads the Graph session, reads device objects,
and optionally writes one local CSV. No tenant object is created, modified,
retired, or deleted.
- Authentication is never initiated by the module. It inspects the existing
session via `Get-MgContext` and fails closed with a named-scope error,
so an unattended run cannot trigger an interactive prompt.
- Least-privilege scope is `Device.Read.All`; `Directory.Read.All` is accepted
only as a documented broader alternative.
- No user principal names, owner identities, IP addresses, or location data
are emitted. Only device inventory attributes appear in the report.
FileList
- EntraDeviceReport.nuspec
- EntraDeviceReport.psd1
- EntraDeviceReport.psm1
- en-US\about_EntraDeviceReport.help.txt
Version History
| Version | Downloads | Last updated |
|---|---|---|
| 0.3.2 (current version) | 5 | 9/9/2026 |