FindObject.Intune.psm1

#Requires -Version 5.1

#region Microsoft Graph / Intune Integration

$script:GraphToken = $null
$script:GraphTokenExpiry = [datetime]::MinValue
$script:GraphBaseUri = 'https://graph.microsoft.com/v1.0'

function Connect-FindObjectGraph {
    <#
    .SYNOPSIS
        Authenticates to Microsoft Graph for Intune queries.
 
    .DESCRIPTION
        Acquires an OAuth2 token for Microsoft Graph using either the device code flow
        (interactive, default) or client credentials (automation/CI). The token is cached
        in-memory for the session and automatically refreshed when expired.
 
        Supports the standard Intune/DeviceManagement scopes. For read-only operations
        (all Get-Intune* commands), DeviceManagement.ManagedDevices.Read.All and
        DeviceManagementApps.Read.All are sufficient.
 
    .PARAMETER TenantId
        Your Azure AD tenant ID or domain (e.g. "contoso.onmicrosoft.com" or a GUID).
 
    .PARAMETER ClientId
        App registration client ID. Defaults to the well-known Microsoft PowerShell
        first-party app (14d82eec-204b-4c2f-b7e8-296a70dab67e) which supports device code flow.
 
    .PARAMETER ClientSecret
        Client secret for app-only (daemon) authentication. When provided, uses the
        client_credentials grant instead of device code flow.
 
    .PARAMETER Certificate
        X509Certificate2 for certificate-based app authentication.
 
    .PARAMETER Scope
        OAuth2 scopes to request. Defaults to common Intune read scopes.
 
    .EXAMPLE
        Connect-FindObjectGraph -TenantId "contoso.onmicrosoft.com"
 
        Interactive device code flow — displays a code to enter at microsoft.com/devicelogin.
 
    .EXAMPLE
        Connect-FindObjectGraph -TenantId $tenantId -ClientId $appId -ClientSecret $secret
 
        Non-interactive app-only auth for CI/CD pipelines.
 
    .LINK
        Disconnect-FindObjectGraph
        Get-IntuneDevice
    #>

    [CmdletBinding(DefaultParameterSetName = 'DeviceCode')]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$TenantId,

        [Parameter(Position = 1)]
        [string]$ClientId = '14d82eec-204b-4c2f-b7e8-296a70dab67e',

        [Parameter(ParameterSetName = 'ClientSecret', Mandatory = $true)]
        [string]$ClientSecret,

        [Parameter(ParameterSetName = 'Certificate', Mandatory = $true)]
        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [string[]]$Scope = @(
            'https://graph.microsoft.com/DeviceManagement.ManagedDevices.Read.All'
            'https://graph.microsoft.com/DeviceManagementApps.Read.All'
            'https://graph.microsoft.com/DeviceManagementConfiguration.Read.All'
        )
    )

    $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"

    switch ($PSCmdlet.ParameterSetName) {
        'DeviceCode' {
            # Step 1: Request device code
            $dcBody = @{
                client_id = $ClientId
                scope     = ($Scope -join ' ')
            }
            $dcResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/devicecode" `
                -Method Post -Body $dcBody -ContentType 'application/x-www-form-urlencoded'

            Write-Host ""
            Write-Host $dcResponse.message -ForegroundColor Cyan
            Write-Host ""

            # Step 2: Poll for token
            $interval = [int]$dcResponse.interval
            $expiresIn = [int]$dcResponse.expires_in
            $deadline = (Get-Date).AddSeconds($expiresIn)

            while ((Get-Date) -lt $deadline) {
                Start-Sleep -Seconds $interval
                try {
                    $tokenBody = @{
                        grant_type  = 'urn:ietf:params:oauth:grant-type:device_code'
                        client_id   = $ClientId
                        device_code = $dcResponse.device_code
                    }
                    $tokenResponse = Invoke-RestMethod -Uri $tokenEndpoint `
                        -Method Post -Body $tokenBody -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop

                    $script:GraphToken = $tokenResponse.access_token
                    $script:GraphTokenExpiry = (Get-Date).AddSeconds($tokenResponse.expires_in - 60)
                    Write-Verbose "FindObject Graph: authenticated via device code flow."
                    Write-Host "Connected to Microsoft Graph ($TenantId)" -ForegroundColor Green
                    return
                } catch {
                    $err = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue
                    if ($err.error -eq 'authorization_pending') { continue }
                    if ($err.error -eq 'slow_down') { $interval += 5; continue }
                    throw "Device code authentication failed: $($err.error_description)"
                }
            }
            throw "Device code expired before authentication completed."
        }

        'ClientSecret' {
            $tokenBody = @{
                grant_type    = 'client_credentials'
                client_id     = $ClientId
                client_secret = $ClientSecret
                scope         = 'https://graph.microsoft.com/.default'
            }
            $tokenResponse = Invoke-RestMethod -Uri $tokenEndpoint `
                -Method Post -Body $tokenBody -ContentType 'application/x-www-form-urlencoded'

            $script:GraphToken = $tokenResponse.access_token
            $script:GraphTokenExpiry = (Get-Date).AddSeconds($tokenResponse.expires_in - 60)
            Write-Verbose "FindObject Graph: authenticated via client credentials."
            Write-Host "Connected to Microsoft Graph ($TenantId) [app-only]" -ForegroundColor Green
        }

        'Certificate' {
            # Build client assertion JWT (simplified — for production use a proper JWT library)
            throw "Certificate-based auth requires a JWT assertion builder. Use -ClientSecret for automation, or device code flow for interactive sessions."
        }
    }
}

function Disconnect-FindObjectGraph {
    <#
    .SYNOPSIS
        Clears the cached Microsoft Graph token.
 
    .EXAMPLE
        Disconnect-FindObjectGraph
    #>

    [CmdletBinding()]
    param()

    $script:GraphToken = $null
    $script:GraphTokenExpiry = [datetime]::MinValue
    Write-Verbose "FindObject Graph: token cleared."
}

function Invoke-FindObjectGraphRequest {
    <#
    Internal: paginated GET against Microsoft Graph. Returns all pages as a flat array.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Uri,

        [hashtable]$QueryParameters = @{}
    )

    if (-not $script:GraphToken -or (Get-Date) -ge $script:GraphTokenExpiry) {
        throw "Not connected to Microsoft Graph. Run Connect-FindObjectGraph first."
    }

    $headers = @{
        Authorization  = "Bearer $script:GraphToken"
        'Content-Type' = 'application/json'
    }

    # Build query string
    if ($QueryParameters.Count -gt 0) {
        $qs = ($QueryParameters.GetEnumerator() | ForEach-Object {
            "$([uri]::EscapeDataString($_.Key))=$([uri]::EscapeDataString($_.Value))"
        }) -join '&'
        $Uri = "$Uri`?$qs"
    }

    $results = [System.Collections.Generic.List[object]]::new()
    $nextUri = $Uri

    do {
        $response = Invoke-RestMethod -Uri $nextUri -Headers $headers -Method Get
        foreach ($item in $response.value) { $results.Add($item) }
        $nextUri = $response.'@odata.nextLink'
        Write-Verbose "FindObject Graph: fetched $($results.Count) objects so far..."
    } while ($nextUri)

    return $results
}

function Get-IntuneDevice {
    <#
    .SYNOPSIS
        Retrieves managed devices from Intune via Microsoft Graph.
 
    .DESCRIPTION
        Queries the /deviceManagement/managedDevices endpoint and returns device objects
        that pipe directly into Find-ObjectByName for filtering.
 
    .PARAMETER Filter
        Optional OData $filter expression passed to Graph (server-side filtering).
        For client-side filtering, pipe to fob instead.
 
    .PARAMETER Select
        Properties to retrieve. Defaults to common identification fields.
 
    .PARAMETER Top
        Maximum number of results per page (Graph default is 1000).
 
    .EXAMPLE
        Get-IntuneDevice | fob "TONY"
 
        Find all devices with "TONY" in the displayName.
 
    .EXAMPLE
        Get-IntuneDevice | fob "windows NOT compliant" -Property operatingSystem,complianceState
 
    .EXAMPLE
        Get-IntuneDevice -Filter "operatingSystem eq 'iOS'" | fob "ipad" -First 5
 
    .LINK
        Connect-FindObjectGraph
        Find-ObjectByName
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [string]$Filter,

        [string[]]$Select = @('id', 'displayName', 'operatingSystem', 'osVersion',
            'complianceState', 'managedDeviceOwnerType', 'deviceEnrollmentType',
            'lastSyncDateTime', 'userPrincipalName', 'serialNumber', 'model', 'manufacturer'),

        [ValidateRange(1, 1000)]
        [int]$Top = 1000
    )

    $params = @{
        '$select' = ($Select -join ',')
        '$top'    = "$Top"
    }
    if ($Filter) { $params['$filter'] = $Filter }

    $uri = "$script:GraphBaseUri/deviceManagement/managedDevices"
    Invoke-FindObjectGraphRequest -Uri $uri -QueryParameters $params
}

function Get-IntuneApp {
    <#
    .SYNOPSIS
        Retrieves mobile apps from Intune via Microsoft Graph.
 
    .EXAMPLE
        Get-IntuneApp | fob "chrome OR edge NOT update"
 
    .EXAMPLE
        Get-IntuneApp | fob "teams" -Property displayName -Highlight
 
    .LINK
        Connect-FindObjectGraph
        Find-ObjectByName
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [string]$Filter,

        [string[]]$Select = @('id', 'displayName', 'publisher', 'platform',
            'appAvailability', 'createdDateTime', 'lastModifiedDateTime', 'isAssigned'),

        [ValidateRange(1, 1000)]
        [int]$Top = 1000
    )

    $params = @{
        '$select' = ($Select -join ',')
        '$top'    = "$Top"
    }
    if ($Filter) { $params['$filter'] = $Filter }

    $uri = "$script:GraphBaseUri/deviceAppManagement/mobileApps"
    Invoke-FindObjectGraphRequest -Uri $uri -QueryParameters $params
}

function Get-IntuneCompliancePolicy {
    <#
    .SYNOPSIS
        Retrieves device compliance policies from Intune.
 
    .EXAMPLE
        Get-IntuneCompliancePolicy | fob "windows"
 
    .EXAMPLE
        Get-IntuneCompliancePolicy | fob "bitlocker OR encryption" -Property displayName
 
    .LINK
        Connect-FindObjectGraph
        Find-ObjectByName
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [string]$Filter,

        [string[]]$Select = @('id', 'displayName', 'description', 'createdDateTime',
            'lastModifiedDateTime', 'platforms', 'scheduledActionsForRule'),

        [ValidateRange(1, 1000)]
        [int]$Top = 1000
    )

    $params = @{
        '$select' = ($Select -join ',')
        '$top'    = "$Top"
    }
    if ($Filter) { $params['$filter'] = $Filter }

    $uri = "$script:GraphBaseUri/deviceManagement/deviceCompliancePolicies"
    Invoke-FindObjectGraphRequest -Uri $uri -QueryParameters $params
}

function Get-IntuneConfigProfile {
    <#
    .SYNOPSIS
        Retrieves device configuration profiles from Intune.
 
    .EXAMPLE
        Get-IntuneConfigProfile | fob "wifi OR vpn"
 
    .EXAMPLE
        Get-IntuneConfigProfile | fob "restrict" -Property displayName -Count
 
    .LINK
        Connect-FindObjectGraph
        Find-ObjectByName
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [string]$Filter,

        [string[]]$Select = @('id', 'displayName', 'description', 'createdDateTime',
            'lastModifiedDateTime', 'platforms', 'deviceManagementApplicabilityRuleOsEdition'),

        [ValidateRange(1, 1000)]
        [int]$Top = 1000
    )

    $params = @{
        '$select' = ($Select -join ',')
        '$top'    = "$Top"
    }
    if ($Filter) { $params['$filter'] = $Filter }

    $uri = "$script:GraphBaseUri/deviceManagement/deviceConfigurations"
    Invoke-FindObjectGraphRequest -Uri $uri -QueryParameters $params
}

function Get-IntuneAutopilotDevice {
    <#
    .SYNOPSIS
        Retrieves Windows Autopilot device identities from Intune.
 
    .EXAMPLE
        Get-IntuneAutopilotDevice | fob "23H2"
 
    .EXAMPLE
        Get-IntuneAutopilotDevice | fob "dell OR lenovo" -Property manufacturer,model
 
    .LINK
        Connect-FindObjectGraph
        Find-ObjectByName
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [string]$Filter,

        [string[]]$Select = @('id', 'serialNumber', 'productKey', 'manufacturer', 'model',
            'groupTag', 'enrollmentState', 'deploymentProfileAssignmentStatus'),

        [ValidateRange(1, 1000)]
        [int]$Top = 1000
    )

    $params = @{
        '$select' = ($Select -join ',')
        '$top'    = "$Top"
    }
    if ($Filter) { $params['$filter'] = $Filter }

    $uri = "$script:GraphBaseUri/deviceManagement/windowsAutopilotDeviceIdentities"
    Invoke-FindObjectGraphRequest -Uri $uri -QueryParameters $params
}

function Get-IntuneEnrollment {
    <#
    .SYNOPSIS
        Retrieves device enrollment configurations (enrollment restrictions) from Intune.
 
    .EXAMPLE
        Get-IntuneEnrollment | fob "limit OR restrict"
 
    .LINK
        Connect-FindObjectGraph
        Find-ObjectByName
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    param(
        [string]$Filter,

        [string[]]$Select = @('id', 'displayName', 'description', 'platformRestrictions',
            'createdDateTime', 'lastModifiedDateTime', 'priority'),

        [ValidateRange(1, 1000)]
        [int]$Top = 1000
    )

    $params = @{
        '$select' = ($Select -join ',')
        '$top'    = "$Top"
    }
    if ($Filter) { $params['$filter'] = $Filter }

    $uri = "$script:GraphBaseUri/deviceManagement/deviceEnrollmentConfigurations"
    Invoke-FindObjectGraphRequest -Uri $uri -QueryParameters $params
}

#endregion

New-Alias -Name gid -Value Get-IntuneDevice -Force

Export-ModuleMember -Function @(
    'Connect-FindObjectGraph',
    'Disconnect-FindObjectGraph',
    'Get-IntuneDevice',
    'Get-IntuneApp',
    'Get-IntuneCompliancePolicy',
    'Get-IntuneConfigProfile',
    'Get-IntuneAutopilotDevice',
    'Get-IntuneEnrollment'
) -Alias @('gid')