EntraDeviceReport.psd1
|
# # Module manifest for module 'EntraDeviceReport' # # Generated by: Olamide Olaleye # @{ # Script module or binary module file associated with this manifest. RootModule = 'EntraDeviceReport.psm1' # Version number of this module. ModuleVersion = '0.3.2' # Supported PSEditions CompatiblePSEditions = @('Core') # ID used to uniquely identify this module GUID = 'bf9454ac-5d27-47d9-b086-98764d7a9af9' # Author of this module Author = 'Olamide Olaleye' # Company or vendor of this module CompanyName = 'Fountview Solutions Limited' # Copyright statement for this module Copyright = '(c) Olamide Olaleye. All rights reserved.' # Description of the functionality provided by this module Description = '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 version of the PowerShell engine required by this module PowerShellVersion = '7.4' # Name of the PowerShell host required by this module # PowerShellHostName = '' # Minimum version of the PowerShell host required by this module # PowerShellHostVersion = '' # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. # DotNetFrameworkVersion = '' # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. # ClrVersion = '' # Processor architecture (None, X86, Amd64) required by this module # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module RequiredModules = @() # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() # Script files (.ps1) that are run in the caller's environment prior to importing this module. # ScriptsToProcess = @() # Type files (.ps1xml) to be loaded when importing this module # TypesToProcess = @() # Format files (.ps1xml) to be loaded when importing this module # FormatsToProcess = @() # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @('Connect-EntraDeviceReport','Get-EntraUnmanagedDeviceReport') # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() # Variables to export from this module VariablesToExport = @() # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @() # DSC resources to export from this module DscResourcesToExport = @() # List of all modules packaged with this module # ModuleList = @() # List of all files packaged with this module # FileList = @() # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. # Tags = @() # A URL to the license for this module. # LicenseUri = '' # A URL to the main website for this project. # ProjectUri = '' # A URL to an icon representing this module. # IconUri = '' # Tags drive PowerShell Gallery discoverability. PSEdition_Core is the # conventional marker for a module that requires PowerShell 6 or later. Tags = @( 'Entra', 'EntraID', 'AzureAD', 'MicrosoftGraph', 'Graph', 'Device', 'DeviceManagement', 'Intune', 'Security', 'Compliance', 'Audit', 'Reporting', 'ReadOnly', 'PSEdition_Core' ) # A URL to the license for this module. LicenseUri = 'https://github.com/Princetimber/EntraDeviceReport/blob/main/LICENSE' # A URL to the main website for this project. ProjectUri = 'https://github.com/Princetimber/EntraDeviceReport' # ReleaseNotes of this module ReleaseNotes = '## [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. ' # Prerelease string of this module Prerelease = '' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false # External dependent modules of this module # ExternalModuleDependencies = @() } # End of PSData hashtable } # End of PrivateData hashtable # HelpInfo URI of this module # HelpInfoURI = '' # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' } |