Microsoft Graph Query Development Tool

Overview

I created this application, Microsoft Graph Query Development Tool, to allow you to connect to your Microsoft Graph API to run REST requests. This is a development tool and allows you to work with Microsoft Graph data as an array that is returned when running the tool. You can also simply run the tool and view the data, but the intended purpose is to pipe the data into another application.

Microsoft Graph basics and how they apply to this tool

Microsoft Graph is the API that sits directly below your Intune Console and is the method to interact with live Intune data (or other online Microsoft services, such as Outlook, Office 365 and Azure). When you use your Intune console, the console is actually making a REST API request/response pair with Microsoft Graph where a GET method is sent with a request URI and a MIME-encoded response object is returned in the HTTP response body back to the Intune console.

For example, if you open your Microsoft Edge Browser and connect to your Azure Portal https://portal.azure.com/ you can view the Microsoft Graph request URI that Intune makes to retrieve the data displayed in the console.

For example open Intune -> Devices -> Compliant Devices. Now, open the Developer Options by hitting F12. Click the Network Column and search for requests to https://graph.microsoft.com. On the right hand side, under Headers, you will see you exact request URI:

In this example, the request URI was to retrieve my compliant devices:

https://graph.microsoft.com/beta/deviceManagement/managedDevices?$filter=(((managementAgent eq ‘mdm’) or (managementAgent eq ‘easMdm’)))&$top=25&_=1518599459508

Deconstructing the request URI, you can begin to understand the components that will be used with my development tool to make the same request:

API Version: Beta
Resource: deviceManagement/managedDevices
Query: (((managementAgent eq ‘mdm’) or (managementAgent eq ‘easMdm’)))&$top=25&_=1518599459508
Query Type: filter

Note these values and how they were derived from the request URI, as they will be used as the inputs for the tool. This is a request URI that returns compliant devices, and running the request within the tool will return the same JSON content to you, but in a form you can work on within an application by converting it to a PowerShell object.

Getting Your Environment Ready for Microsoft Graph Query Development Tool

This application was written in PowerShell and needs to be connected to the internet in order to run. It also requires the AzureAD module for ADAL (Azure Active Directory Authentication Libraries) authentication. It will attempt to download this module automatically if you do not already have it installed. However, feel free to get your environment ready by running:


  Install-Module AzureAD

Installing the Microsoft Graph Query Development Tool

Download the required files below and unzip the files into a directory of your choosing.


Once downloaded, modify the main application file, Invoke-MicrosoftGraphRequestTool.ps1, and change this $user single value at the bottom of the script to match the administrative account for either Intune, AzureAD or whatever cloud application you want to make the Microsoft Graph request URI against.


  $user = youradminaccount@company.onmicrosoft.com

Running the Maiolo Microsoft Graph Query Development Tool

To run the application, run the Invoke-MicrosoftGraphRequestTool.ps1 file within PowerShell and you will immediately be prompted to log into your Azure tenant. Although this login method may seem similar to some of my other tools, the underlying authentication method is different when connecting to the Intune Data Warehouse vs running a Microsoft Graph Request, so any authentication tokens you have open with my other tools will be insufficient here without logging in again.

Once logged in, you will be presented with the application’s main menu:

Run Custom Graph Request

This is the most basic version of a request URI and does not support a query. It only asks for the API version, which is typically “Beta” or “v1.0” and the Resource, such as “deviceAppManagement/mobileApps”, which will return all of the Mobile Applications in your Intune environment.

Run Custom Graph Request (GUI Assisted)

This is the most advanced portion of the application and will allow you to input a full Microsoft Graph request URI, including an optional query. This will bring up a small GUI window where you can build your request URI. Remember the request URI we found from our Intune console on the F12 development box earlier? Let’s use that URI to build our request URI within the application so we can return the same results.

A request URI is then built and invoked with the proper GET request.

This data is then returned, where we can either pass it on to the pipeline or just simply view it:

For example, we could make a small modification to Invoke-MicrosoftGraphRequestTool.ps1 and pass the data to Out-Gridview:


  Invoke-MicrosoftGraphRequestTool -user $user | Out-GridView

Now, when we re-run the application, the data is piped and viewed in Out-GrideView.

Replace Out-GridView with whatever application you would want to next see the data. That’s how the tool works!

The Invocation Function: Invoke-MicrosoftGraphRequestTool


<#
.Synopsis
    Connect to Microsoft Graph to GET a Microsoft Graph request URI JSON content return. This is a development tool to return Graph API content which can be fed to the pipleline to be ingested by another application.
.Author
    David Maiolo
.Help
    $user variable is your InTune tenant admin account such as admin@yourcompany.onmicrosoft.com
        If you are super new to all of this and dont even know what this is, create one by searching "Intune trial" and go through the process
#>

function Invoke-MicrosoftGraphRequestTool{

    Param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=0)]
                   [ValidateScript({$_ -like '*onmicrosoft.com*'})]
                   [string]$user
    )

    #Welcome Screen
    clear
    Write-Host "==============================================="-ForegroundColor Cyan
    Write-Host "Maiolo Microsoft Graph Query Development Tool"-ForegroundColor Cyan
    Write-Host "==============================================="-ForegroundColor Cyan
    Write-Host "v1.0 (2018-02-13) by dmaiolo"-ForegroundColor Cyan
    Write-Host "Connect to Microsoft Graph to Run Graph GET Requests. This is a "
    Write-Host "development tool to return graph data which can be fed to the "
    Write-Host "pipleline to be ingested by another application."
    Write-Host "========================================="-ForegroundColor Cyan

    #Import Local Modules
    $ModulePath = "$PSScriptRoot\MicrosoftGraphRequestToolModules.psm1"
    $localmodules = @("$ModulePath")
    foreach ($localmodule in $localmodules){
        if (Test-Path "$ModulePath") {
            Write-Host "Confirmed: Module Path $ModulePath exists."
            try{
                Write-Host "Importing $localmodule..."
                Import-Module $localmodule -Force
                }
            catch{
                throw "Error: Could not import $localmodule."
            }
        }else {
            throw "Error: Module Path '$ModulePath' doesn't exist."
        }
    }
    
    $modules = @("AzureAD")
    #Install Azure AD modules 
    foreach ($module in $modules){
        if (Get-Module -ListAvailable -Name $module) {
            Write-Host "Confirmed: $module module was already imported."
        } else {
            try{
                Write-Host "$module module does not exist. Installing $module..." -ForegroundColor Yellow
                Install-Module $module
            }catch{
                throw "Could not install required module $module. Tried to run `"Install-Module $module`" but it didn't work."

            }
        }
    }

    # Checking if authToken exists before running authentication
    if($global:authToken){
        #Get Universal Date and Time
        $DateTime = (Get-Date).ToUniversalTime()
        # If the authToken exists checking when it expires
        $TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes

        if($TokenExpires -le 0){
            write-host "Warning: Authentication Token expired" $TokenExpires "minutes ago." -ForegroundColor Yellow
            $global:authToken = Get-AuthToken -User $User
        }else{
            Write-Host "User $user has already been authenticated. (against Microsoft Graph)"
        }
    }else{
        $global:authToken = Get-AuthToken -User $User
    }
    sleep 5

    #Menu
    $Collections = Get-MicrosoftGraphMainCategories
    $results = Get-IntuneMainMenu -Collections $Collections -CollectionName "Maiolo Microsoft Graph Query Development Tool"
    Write-Host "===================================="
    Write-Host "Summary of Graph Query Findings"
    Write-Host "===================================="
    Write-Host "Number of values returned:"($results.count)
    $i=0
    #Write-Host "Summary of First 5 Findings..."
    #$results | select DisplayName,id -First 5| FT -AutoSize
    #Write-Host "Returning full results..."
    #sleep 5

    return  $results

}

$user = "user@company.onmicrosoft.com"

Invoke-MicrosoftGraphRequestTool -user $user

The Methods: MicrosoftGraphRequestToolModules.psm1


#Functions Specific TO Microsoft Graph

function Get-AuthToken {

<#
.SYNOPSIS
This function is used to authenticate with the Graph API REST interface
.DESCRIPTION
The function authenticate with the Graph API Interface with the tenant name
.EXAMPLE
Get-AuthToken
Authenticates you with the Graph API interface
.NOTES
NAME: Get-AuthToken
#>

[cmdletbinding()]

param
(
    [Parameter(Mandatory=$true)]
    $User
)

$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User

$tenant = $userUpn.Host

Write-Host "Checking for AzureAD module..."

    $AadModule = Get-Module -Name "AzureAD" -ListAvailable

    if ($AadModule -eq $null) {

        Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview"
        $AadModule = Get-Module -Name "AzureADPreview" -ListAvailable

    }

    if ($AadModule -eq $null) {
        write-host
        write-host "AzureAD Powershell module not installed..." -f Red
        write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow
        write-host "Script can't continue..." -f Red
        write-host
        exit
    }

# Getting path to ActiveDirectory Assemblies
# If the module count is greater than 1 find the latest version

    if($AadModule.count -gt 1){

        $Latest_Version = ($AadModule | select version | Sort-Object)[-1]

        $aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }

            # Checking if there are multiple versions of the same module found

            if($AadModule.count -gt 1){

            $aadModule = $AadModule | select -Unique

            }

        $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
        $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

    }

    else {

        $adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
        $adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"

    }

[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null

[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null

$clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"

$redirectUri = "urn:ietf:wg:oauth:2.0:oob"

$resourceAppIdURI = "https://graph.microsoft.com"

$authority = "https://login.microsoftonline.com/$Tenant"

    try {

    $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

    # https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx
    # Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession

    $platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"

    $userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")

    $authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result

        # If the accesstoken is valid then create the authentication header

        if($authResult.AccessToken){

        # Creating header for Authorization token

        $authHeader = @{
            'Content-Type'='application/json'
            'Authorization'="Bearer " + $authResult.AccessToken
            'ExpiresOn'=$authResult.ExpiresOn
            }

        return $authHeader

        }

        else {

        Write-Host
        Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red
        Write-Host
        break

        }

    }

    catch {

    write-host $_.Exception.Message -f Red
    write-host $_.Exception.ItemName -f Red
    write-host
    break

    }

}

function Get-MicrosoftGraphMainCategories{
<#
.SYNOPSIS
    This function is used to provide the Main applicaiton menu what to display and Data warehouse collections to call when invoked.
.Author
    David Maiolo
.NOTES
    NAME: Get-MicrosoftGraphMainCategories
#>

    $GraphCategories = @()
    $GraphCategories += [PSCustomObject] @{'name' = 'Get-MicrosoftGraphRESTRequest'; 'DisplayName' = 'Run Custom Graph Request'};
    $GraphCategories += [PSCustomObject] @{'name' = 'Show-Command Get-MicrosoftGraphRESTRequest'; 'DisplayName' = 'Run Custom Graph Request (GUI Assisted)'};
    $GraphCategories += [PSCustomObject] @{'name' = 'Get-MicrosoftGraphRESTRequest -graphApiVersion "Beta" -Resource "deviceAppManagement/mobileApps"'; 'DisplayName' = 'Run Sample Graph Request: Mobile Apps'};
    
    return $GraphCategories
}

function Get-MicrosoftGraphRESTRequest{
<#
.SYNOPSIS
    This function is used to call your Microsoft Graph request of choice and return the results as an array.
.Author
    David Maiolo
.NOTES
    NAME: Get-MicrosoftGraphRESTRequest
#>
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=0)]
        [ValidateSet ("Beta","v1.0")]
        [string]$graphApiVersion,
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=1)]
        [String]$Resource,
        [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=2)]
        [ValidateSet ("count","expand","filter","format","orderby","search","select","skip","skipToken","top")]
        [String]$QueryType,
        [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=3)]
        [String]$Query

    )

    try{
        if(([bool]($MyInvocation.BoundParameters.Keys -contains 'Query')) -and ([bool]($MyInvocation.BoundParameters.Keys -contains 'QueryType'))){
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)?`$$QueryType=$Query"
        }else{
            $uri = "https://graph.microsoft.com/$graphApiVersion/$($resource)"
        }
        Write-Host "Requesting $uri..."

        $GraphRequestValue = (Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value | ? { ($_.'@odata.type').Contains("managed") }

        return $GraphRequestValue

    }catch{
        $ex = $_.Exception
        $errorResponse = $ex.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($errorResponse)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        Write-Host "Response content:`n$responseBody" -ForegroundColor Red
        Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
        write-host
        break
        
    }

}

#Universal Functions

function Convert-VariableNameToFriendlyName{
<#
.SYNOPSIS
    This function is used to convert a variable name, such as helloMyNameIsDavidMaiolo to a friendly variable name, such as Hello My Name Is David Maiolo.
.Author
    David Maiolo
.NOTES
    NAME: Convert-VariableNameToFriendlyName
#>
    param(
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=0)]
        [String]$VariableName
    )

    $FriendlyVariableName = ($VariableName -split '_')[0] -creplace '(?<=\w)([A-Z])', ' $1'
    $TextInfo = (Get-Culture).TextInfo

    $FriendlyVariableName = $TextInfo.ToTitleCase($FriendlyVariableName)

    return $FriendlyVariableName
}

function Get-IntuneMainMenu {
<#
.SYNOPSIS
    This function is used to provide a main menu for the application. This was written generically and can be used for other applications.
.Author
    David Maiolo
.NOTES
    NAME: Get-IntuneMainMenu
#>
    Param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=0)]
                   $Collections,
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=1)]
                   [String]$CollectionName,
        [Parameter(Mandatory=$false,ValueFromPipeline=$false,Position=2)]
                   [Switch]$UseSubMenu
    )

    #if (!($global:intuneWarehouseAuthResult) -or !($global:authToken)) {
    #    throw "No authentication context. Authenticate first!"
    #}
    try {
        do {
            clear
            Write-Host "========================================="
            Write-Host $CollectionName
            Write-Host "========================================="
            #$menu = @{}
            $MenuArray = @()
        
            for ($i=1;$i -le $collections.count; $i++) { 
                Write-Host "[$i] $($collections[$i-1].DisplayName)"
                if($i -eq $Collections.count){
                    $iplusone = ($i+1)
                    Write-Host "[$iplusone] Exit"
                }
            }
            [int]$ans = Read-Host 'Enter Selection'

            if ($ans -eq ($collections.count+1)){
                break
            }else{
                $collection = Invoke-Expression $collections[$ans-1].Name
                #Write-Host Colection: $collection
                if(([bool]($MyInvocation.BoundParameters.Keys -contains 'UseSubMenu'))){
                    Get-IntuneSubMenu -Collections $collection -CollectionName $collections[$ans-1].DisplayName
                }else{
                    return $collection
                }
            }
        }while($ans -ne ($collections.count+1))
    }catch{
        $ex = $_.Exception
        $errorResponse = $ex.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($errorResponse)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        Write-Host "Response content:`n$responseBody" -ForegroundColor Red
        write-host
        sleep 10
        break
    }
}

function Get-IntuneSubMenu {
<#
.SYNOPSIS
    This function is used to provide a sub menu for the application. This was written generically and can be used for other applications.
.Author
    David Maiolo
.NOTES
    NAME: Get-IntuneSubMenu
#>
    Param
    (
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=0)]
                   $Collections,
        [Parameter(Mandatory=$true,ValueFromPipeline=$false,Position=1)]
                   [String]$CollectionName
    )

    #$collections = Get-IntuneDatawarehouseCollections

    if (!$global:intuneWarehouseAuthResult) {
        throw "No authentication context. Authenticate first by running 'Connect-IntuneDataWarehouse'"
    }

    try {
        do{
            clear
            Write-Host ">>>>>$CollectionName                          " -ForegroundColor Cyan
            Write-Host ">>>>>=========================================" -ForegroundColor Cyan
            $menu = @{}
            for ($i=1;$i -le $collections.count; $i++) { 
                Write-Host "     [$i] $($collections[$i-1].DisplayName)" 
                $menu.Add($i,($collections[$i-1].name))
                if($i -eq $Collections.count){
                    $iplusone = ($i+1)
                    Write-Host "     [$iplusone] Go Back"
                    $menu.Add($i+1,("GoBack"))
                }
            }
        
            [int]$ans = Read-Host 'Enter Selection'
            $selection = $menu.Item($ans);
        
            if ($selection -eq "GoBack"){
                #Menu
                #$Collections = Get-IntuneDatawarehouseMenuCategories
                #Get-IntuneMainMenu -Collections $Collections -CollectionName "InTune Management"
                return
            }else{
                Get-IntuneDataWarehouseCollection $selection | Out-GridView -Title (Convert-VariableNameToFriendlyName -VariableName $selection) 
                #Read-Host -Prompt "Press Enter to continue" 
            }
        }while($selection -ne "GoBack")

    }catch{
        $ex = $_.Exception
        $errorResponse = $ex.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($errorResponse)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        Write-Host "Response content:`n$responseBody" -ForegroundColor Red
        Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
        write-host
        sleep 10
        break
    }
}


Leave a Comment

Your email address will not be published.