IntuneOperator.psm1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSAvoidAssignmentToAutomaticVariable', 'IsWindows',
    Justification = 'IsWindows doesnt exist in PS5.1'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSUseDeclaredVarsMoreThanAssignments', 'IsWindows',
    Justification = 'IsWindows doesnt exist in PS5.1'
)]
[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Import-PowerShellDataFile -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"

if ($PSEdition -eq 'Desktop') {
    $IsWindows = $true
}

#region [functions] - [private]
Write-Debug "[$scriptName] - [functions] - [private] - Processing folder"
#region [functions] - [private] - [Get-UsersLoggedOnForDevice]
Write-Debug "[$scriptName] - [functions] - [private] - [Get-UsersLoggedOnForDevice] - Importing"
function Get-UsersLoggedOnForDevice {
    <#
    .SYNOPSIS
    Retrieves logged-on user information for an Intune managed device.

    .DESCRIPTION
    Queries a specific managed device by DeviceId and retrieves the usersLoggedOn collection.
    Returns the complete device object including all logged-on user entries.

    .PARAMETER Id
    The Intune managed device identifier (GUID).

    .EXAMPLE
    Get-UsersLoggedOnForDevice -Id "c1f5d1d7-2d2b-4d8c-9f0a-0d2a3d1e2f3a"

    Returns the device object with usersLoggedOn collection for the specified device ID.

    .INPUTS
    System.String

    .OUTPUTS
    PSObject

    .NOTES
    Part of the Intune Device Login helper functions.
    Uses Microsoft Graph /beta endpoint where usersLoggedOn is available.
    Requires DeviceManagementManagedDevices.Read.All scope.
    #>


    [OutputType([PSObject])]
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "The managed device ID (GUID)"
        )]
        [ValidatePattern('^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$')]
        [string]$Id
    )

    begin {
        $baseUri = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices'
    }

    process {
        $uri = "$baseUri/$Id"
        Invoke-GraphGet -Uri $uri
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Get-UsersLoggedOnForDevice] - Done"
#endregion [functions] - [private] - [Get-UsersLoggedOnForDevice]
#region [functions] - [private] - [Invoke-GraphGet]
Write-Debug "[$scriptName] - [functions] - [private] - [Invoke-GraphGet] - Importing"
function Invoke-GraphGet {
    <#
    .SYNOPSIS
    Invokes a GET request against the Microsoft Graph API with error handling.

    .DESCRIPTION
    Wrapper function for making authenticated GET requests to the Microsoft Graph API.
    Provides consistent error handling and verbose output for all Graph API calls.
    Requires an established Microsoft Graph connection via Connect-MgGraph.

    .PARAMETER Uri
    The full URI of the Graph API endpoint to query.

    .EXAMPLE
    Invoke-GraphGet -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices"

    Retrieves all managed devices from the Graph API.

    .INPUTS
    System.String

    .OUTPUTS
    PSObject

    .NOTES
    Part of the Intune Device Login helper functions.
    Requires Microsoft.Graph PowerShell module with active connection.
    #>


    [OutputType([PSObject])]
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "The full URI of the Graph API endpoint"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Uri
    )

    process {
        Write-Verbose "GET $Uri"
        try {
            $splat = @{
                Method      = 'GET'
                Uri         = $Uri
                ErrorAction = 'Stop'
            }
            Invoke-MgGraphRequest @splat
        } catch {
            throw "Graph request failed for '$Uri': $($_.Exception.Message)"
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Invoke-GraphGet] - Done"
#endregion [functions] - [private] - [Invoke-GraphGet]
#region [functions] - [private] - [Resolve-EntraUserById]
Write-Debug "[$scriptName] - [functions] - [private] - [Resolve-EntraUserById] - Importing"
function Resolve-EntraUserById {
    <#
    .SYNOPSIS
    Resolves an Entra ID user by user ID to retrieve user principal name and other details.

    .DESCRIPTION
    Queries Microsoft Graph for a specific user by their user ID (object ID).
    Returns user object including userPrincipalName for reporting and audit purposes.
    Handles cases where user may no longer exist in Entra ID.

    .PARAMETER UserId
    The Entra ID user object identifier (GUID).

    .EXAMPLE
    Resolve-EntraUserById -UserId "d1e1a1d7-2d2b-4d8c-9f0a-0d2a3d1e2f3a"

    Returns the user object with UPN for the specified user ID.

    .INPUTS
    System.String

    .OUTPUTS
    PSObject

    .NOTES
    Part of the Intune Device Login helper functions.
    Uses Microsoft Graph /v1.0 endpoint.
    Requires User.Read.All scope.
    Returns a minimal user object if user cannot be found.
    #>


    [OutputType([PSObject])]
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "The Entra ID user object ID"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$UserId
    )

    begin {
        $baseUri = 'https://graph.microsoft.com/v1.0/users'
    }

    process {
        $uri = "$baseUri/$UserId"
        try {
            Invoke-GraphGet -Uri $uri
        } catch {
            Write-Verbose "Could not resolve user ID '$UserId': $($_.Exception.Message)"
            # Return a minimal object with the ID and a placeholder UPN
            [PSCustomObject]@{
                id                = $UserId
                userPrincipalName = "Unknown (ID: $UserId)"
            }
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Resolve-EntraUserById] - Done"
#endregion [functions] - [private] - [Resolve-EntraUserById]
#region [functions] - [private] - [Resolve-IntuneDeviceByName]
Write-Debug "[$scriptName] - [functions] - [private] - [Resolve-IntuneDeviceByName] - Importing"
function Resolve-IntuneDeviceByName {
    <#
    .SYNOPSIS
    Resolves one or more Intune managed devices by device name.

    .DESCRIPTION
    Queries Intune managed devices using the device name filter.
    Performs case-insensitive exact match searching via OData filter.
    Returns all devices matching the specified name.

    .PARAMETER Name
    The device name to search for in Intune managed devices.

    .EXAMPLE
    Resolve-IntuneDeviceByName -Name "PC-001"

    Returns the managed device object matching the name "PC-001", if found.

    .INPUTS
    System.String

    .OUTPUTS
    PSObject[]

    .NOTES
    Part of the Intune Device Login helper functions.
    Uses Microsoft Graph /beta endpoint.
    Requires DeviceManagementManagedDevices.Read.All scope.
    #>


    [OutputType([System.Collections.Hashtable[]])]
    [CmdletBinding()]
    param(
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "The device name to resolve"
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Name
    )

    begin {
        $baseUri = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices'
    }

    process {
        # deviceName is case-insensitive in OData. Exact match.
        $encoded = [uri]::EscapeDataString("deviceName eq '$Name'")
        $uri = "$baseUri`?`$filter=$encoded"

        $resp = Invoke-GraphGet -Uri $uri

        if ($null -eq $resp.value -or $resp.value.Count -eq 0) {
            Write-Verbose "No managed devices found with deviceName '$Name'."
            return [System.Collections.Hashtable[]]@()
        }

        return $resp.value
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Resolve-IntuneDeviceByName] - Done"
#endregion [functions] - [private] - [Resolve-IntuneDeviceByName]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [completers]
Write-Debug "[$scriptName] - [functions] - [public] - [completers] - Importing"
Register-ArgumentCompleter -CommandName New-PSModuleTest -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters

    'Alice', 'Bob', 'Charlie' | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [completers] - Done"
#endregion [functions] - [public] - [completers]
#region [functions] - [public] - [Device]
Write-Debug "[$scriptName] - [functions] - [public] - [Device] - Processing folder"
#region [functions] - [public] - [Device] - [Get-IntuneDeviceLogin]
Write-Debug "[$scriptName] - [functions] - [public] - [Device] - [Get-IntuneDeviceLogin] - Importing"
function Get-IntuneDeviceLogin {
    <#
    .SYNOPSIS
    Retrieves logged-on user info for an Intune-managed device by DeviceId (GUID) or device name.

    .DESCRIPTION
    Uses Microsoft Graph (beta) to read managed device metadata and the `usersLoggedOn` collection.
    When given a DeviceId, queries that specific device. When given a DeviceName, resolves one or more
    matching managed devices (`deviceName eq '<name>'`) and returns logon info for each match.

    Requires an authenticated Graph session with appropriate scopes.

    Scopes (minimum):
        - DeviceManagementManagedDevices.Read.All
        - User.Read.All

    .PARAMETER DeviceId
    The Intune managed device identifier (GUID). Parameter set: ById.

    .PARAMETER DeviceName
    The device name to resolve in Intune managed devices. Parameter set: ByName.
    If multiple devices share the same name, all matches are processed.

    .EXAMPLE
    Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All","User.Read.All"
    Get-IntuneDeviceLogin -DeviceId "c1f5d1d7-2d2b-4d8c-9f0a-0d2a3d1e2f3a"

    Gets logged-on user info for the specified device.

    .EXAMPLE
    Get-IntuneDeviceLogin -DeviceName PC-001

    Resolves the device name and returns logged-on user info for the match.

    .INPUTS
    System.String (DeviceId via -DeviceId, or DeviceName via -DeviceName with ValueFromPipeline/PropertyName)

    .OUTPUTS
    PSCustomObject with the following properties
    - DeviceId (string)
    - DeviceName (string)
    - UserId (string)
    - UserPrincipalName (string)
    - LastLogonDateTime (datetime)

    .NOTES
    Author: FHN & ChatGPT
    - Uses /beta Graph endpoints because usersLoggedOn is exposed there.
    - Emits no output if no users are logged on for a device.
    - Errors are terminating for request/HTTP failures; use try/catch around calls if desired.
    #>


    [OutputType([PSCustomObject])]
    [CmdletBinding(DefaultParameterSetName = 'ById', SupportsShouldProcess = $false)]
    param(
        # ById: DeviceId (GUID)
        [Parameter(
            ParameterSetName = 'ById',
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidatePattern('^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$')]
        [Alias('Id', 'ManagedDeviceId')]
        [string]$DeviceId,

        # ByName: DeviceName (string)
        [Parameter(
            ParameterSetName = 'ByName',
            Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('Name', 'ComputerName')]
        [string]$DeviceName
    )

    process {
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                Write-Verbose "Resolving usersLoggedOn for device id: $DeviceId"
                $device = Get-UsersLoggedOnForDevice -Id $DeviceId

                if (-not $device) {
                    Write-Verbose "Managed device not found for id '$DeviceId'."
                    return
                }

                if (-not $device.usersLoggedOn -or $device.usersLoggedOn.Count -eq 0) {
                    Write-Verbose "No logged-on users found for device '$($device.deviceName)' ($DeviceId)."
                    return
                }

                foreach ($entry in $device.usersLoggedOn) {
                    $user = Resolve-EntraUserById -UserId $entry.userId
                    [PSCustomObject]@{
                        DeviceId          = $device.id
                        DeviceName        = $device.deviceName
                        UserId            = $entry.userId
                        UserPrincipalName = $user.userPrincipalName
                        LastLogonDateTime = [datetime]$entry.lastLogOnDateTime
                    }
                }
            }

            'ByName' {
                Write-Verbose "Resolving device(s) by name: $DeviceName"
                $devices = Resolve-IntuneDeviceByName -Name $DeviceName

                if ($devices.Count -gt 1) {
                    Write-Verbose "Multiple devices matched name '$DeviceName' ($($devices.Count) matches). Returning results for all."
                }

                foreach ($dev in $devices) {
                    if (-not $dev.usersLoggedOn -or $dev.usersLoggedOn.Count -eq 0) {
                        Write-Verbose "No logged-on users found for device '$($dev.deviceName)' ($($dev.id))."
                        continue
                    }

                    foreach ($entry in $dev.usersLoggedOn) {
                        $user = Resolve-EntraUserById -UserId $entry.userId
                        [PSCustomObject]@{
                            DeviceId          = $dev.id
                            DeviceName        = $dev.deviceName
                            UserId            = $entry.userId
                            UserPrincipalName = $user.userPrincipalName
                            LastLogonDateTime = [datetime]$entry.lastLogOnDateTime
                        }
                    }
                }
            }
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [Device] - [Get-IntuneDeviceLogin] - Done"
#endregion [functions] - [public] - [Device] - [Get-IntuneDeviceLogin]
Write-Debug "[$scriptName] - [functions] - [public] - [Device] - Done"
#endregion [functions] - [public] - [Device]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = 'Get-IntuneDeviceLogin'
    Variable = ''
}
Export-ModuleMember @exports
#endregion Member exporter