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

Copy and Paste the following command to install this package using PowerShellGet More Info

Install-Module -Name EntraDeviceReport

Copy and Paste the following command to install this package using Microsoft.PowerShell.PSResourceGet More Info

Install-PSResource -Name EntraDeviceReport

You can deploy this package directly to Azure Automation. Note that deploying packages with dependencies will deploy all the dependencies to Azure Automation. Learn More

Manually download the .nupkg file to your system's default download location. Note that the file won't be unpacked, and won't include any dependencies. Learn More

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

Core

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

Version History

Version Downloads Last updated
0.3.2 (current version) 5 9/9/2026