Cheating with portals: copying off their Graph API calls

I find myself navigating Graph API quite often, trying to figure out how to query some undocumented attribute, triggering less-than-descriptive error messages, scratching my head wondering how to proceed, in no specific order.

In this post I’m going to explain how to capture, analyze and reproduce the way Microsoft’s own management portals retrieve their data. A little bit of cheating never hurt anyone, and something about reinventing wheels!

I find that taking a good look at their calls to Graph API always helps me along, even if just a little. It (almost) never gives me a clear-cut solution, but at the very least it can tell me if I’m on the right path or just being silly.

Never afraid to ask

Per example, I’m using a question I recently ‘answered’ on Microsoft’s Tech Community. Someone asked a seemingly simple question: how can I get a list of Hybrid-joined devices that are not MDM-enrolled? Turns out, that little negation in there is going to cause a whole lot of trouble.

A word of warning: in this post, I’m setting you up for failure. I will not resolve this request in the end. This time, it’s all about the journey and not the destination😊.

If you came here looking for a solution to this specific case, I suggest you K.I.S.S. and just export all devices (use the “Download devices” button for that), open the resulting .CSV-file in Excel and put a filter on the “MDM”-column to select all the blanks.

So close…

As always, I try to use already available, easily accessible tools first. The Azure AD-portal’s Devices-overview seemed like a good place to start, considering we want to get info on unmanaged devices. You can actually create a list of Hybrid-joined, MDM-managed devices here, by selecting a few filters in the “All devices” view:

  • Join Type: Hybrid Azure AD joined
  • MDM: Microsoft Intune

Now, let’s just adapt the filter to select everything without an MDM-attribute. “Easy!”, I thought, naively.

You see, the problem is that you can’t target empty attributes with most customizable filters in these portals. Unfortunately, that happens to be exactly what we need to do here. But hey, we came so close! Let’s just dive into the inner workings of this view, so we can manipulate it a tiny bit. I’m sure that functionality is just missing in the GUI.

Pop the hood

Let’s have a look-see under the hood. Most of Microsoft’s portals use Graph API as their backend. And, conveniently for us, they call this API asynchronously from our browser, allowing us to capture and inspect every call it makes.

The easiest way to do this is to press F12. In most browsers, this opens something called Developer Tools, docked to the side or on the bottom of your window. It contains all kinds of goodies, but we’re primarily interested in network traffic.  

Heads-up: for this post, I’m using Edge. Other browsers provide tooling like this as well, but it might look a bit different.

Location of Network-tab in Edge's Developer Tools

Depending on your window-size and docking position, the “Network”-tab you’re looking for may be hidden. If so, use the double arrows to access the additional tabs (including “Network”).

Location of Clear-button in Edge's Developer Tools

I like to start things fresh, so clear any traffic captured with the “Clear”-button and refresh the view.

Tip: don’t use your browser’s refresh (like when pressing F5), as this will reload the entire portal. We only want our dataset to refresh, so use the “Refresh”-button the portal provides at the top of the results.

Graph X-Ray

A funny thing happened when I published this blog. Within a couple of minutes someone pointed me to a tweet from Merill Fernando, announcing Graph X-Ray add-on (for Edge, Chrome and Windows Desktop).

This, too, let’s you inspect Graph API calls from your browser, but goes way further and even provides full examples to recreate the calls in PowerShell and whatnot. It’s just been released to the public, so head on over to Merill’s site to try it out.

At the time of writing it was missing an option to inspect Graph API’s responses (which is something I do quite often) but it sure is worth mentioning here!

Call of the wild

Location of Url-column in Edge's Developer Tools

After refreshing the dataset, the list under “Network” will be populated with all kinds of requests. So how do we know which ones are interesting and which ones are not? Well, Graph API-calls are always sent to https://graph.microsoft.com/, so we can easily recognize them by checking the Request Url.

The (Request) “Url”-column isn’t shown by default (at least in Edge it isn’t). You can show it by right-clicking on any column in this requests-list and checking “Url”.

List of network requests in Edge's Developer Tools
As you can see, I tend to customize my column layout completely.

In this case, we are looking for a request to https://graph.microsoft.com/beta/devices. I happen to know this, but how should you? Luckily, these calls follow a specific pattern to tell the API what they’re looking for.

Endpoint

The first part you see is beta. This, combined with the generic address, is called the endpoint. There’s only one other variant, where beta is replaced with v1.0, which is (officially) the only supported endpoint.

There’s nothing wrong with using this “beta”-thingy (and, as you can see, most of Microsoft’s portals do so themselves). Just keep in mind that it’s in perpetual preview and its documentation usually isn’t final either. So, don’t use it for business-critical stuff as things may still shift around (although it rarely does once it becomes part of a portal).

Resource

After the endpoint is declared, the request needs to tell Graph API what resource is going to be accessed. In this call it’s simply /devices, being all devices known in Azure AD. There’s loads of other resources you can access though, like /users or (for us MEM-enthusiasts) /deviceManagement/managedDevices.

As you can see in that last one (which will list all managed devices), there may even be some nesting involved. Usually only Azure AD-resources are accessed at the root level and every other service has its own ‘folder’.

The portals also use Graph API to manipulate configuration, which you can capture like this as well. They will be POST-requests and they work differently. I’m not going to dive into those in this post but be careful with those.

Strip it back

When you click on the request in the “Name”-column, you will be sent to the details screen. It’ll show you all the yummy insides of the request and whatever Graph API’s response was.

For this post, we only need data in the Request Url but please do click around in the “Response” (or better, “Preview”) tab. There’s lots to see in there as well.

If you’ve set the filters I mentioned, you’ll find something like this in the Request Url:

https://graph.microsoft.com/beta/devices?$top=25&$count=true&$orderby=id&$filter=trustType%20eq%20%27serverad%27%20and%20(mdmAppId%20eq%20%270000000a-0000-0000-c000-000000000000%27)

Stripping away the stuff we already explained (and do some URL-decoding), leaves the following:

$top=25&$count=true&$orderby=id&$filter=trustType%20eq%20%27serverad%27%20and%20(mdmAppId%20eq%20%270000000a-0000-0000-c000-000000000000%27)

We don’t really care about the first bits: $top sets the number of results per page, $count adds a total count of results, regardless of page size and $orderby (duh).

This $filter-thing is interesting, though. If we URL-decode it, we’re left with:

trustType eq 'serverad' and (mdmAppId eq '0000000a-0000-0000-c000-000000000000')

That, my friends, is the filter the portal uses to construct its view. It hold a few little nuggets of info:

  • The attribute trustType tells us the join-state of the device. The value serverad means it is Hybrid-joined. Other options would be “azuread” and “workplace”, reflecting Azure AD-joined and Azure AD-registered devices.
  • The attribute mdmAppId tells us if and how the device is MDM-enrolled. The value 0000000a-0000-0000-c000-000000000000 means it’s managed by Intune. Just trust me on that one.

Do it again!

We can now reconstruct this request. You could use Graph Explorer but I like things done in PowerShell and here we can use the handy Microsoft.Graph.Authentication module:

$filter = "trustType eq 'serverad' and mdmAppId eq '0000000a-0000-0000-c000-000000000000'";
$uri = "https://graph.microsoft.com/beta/devices?`$filter=$([uri]::EscapeDataString($filter))";
Invoke-MgGraphRequest $uri -Method GET;

Notice the backtick (the ` -character) before $filter? That single character has screwed me many times (and will probably do so a few times more). It escapes the dollar-sign so it’s not interpreted as the start of a variable. If you forget it, the text $filter would be replaced with the contents of the $filter-variable.

Yes, I know, you can also use single quotes to disable that behavior, but that just means I need to escape other characters and this works better (for me).

You can also use the Get-MgGraphDevice cmdlet, but that requires the additional Microsoft.Graph.Identity.DirectoryManagement module. In the end, this would result in the same Graph API call.

Drawing a blank

We’re going to tweak the filter a bit and try to get the result we need (reminder: we’re trying to list all Hybrid-joined devices not managed by Intune).

Oh, by the way, your first thought is wrong.

You were going to say we just need to look for empty mdmAppId-attributes, right? Well… annoyingly, Graph API isn’t to good at filtering on empty, null or blatantly non-existent values and will usually just throw out a generic error, and it will do so here.

Generic Graph API error in Powershell: Bad Request

Hey, remember we can’t do that in the portals either? This is probably why. At least now we know we’re not idiots for not being able to figure it out 😊.

That’s another reason why I’m using PowerShell here: it provides an alternative way to filter for empty attributes. We will need to get the (partially) unfiltered dataset (yuck, inefficient), and then drill-down further with PowerShell. Maybe something like this:

Get-MgDevice -All -Filter "trustType eq 'serverad'" | Where-Object { (-not($_.mdmAppId) -or ($_.mdmAppId -eq '')) };

I warned you I was setting you up for failure, didn’t I?

You might wonder why I’m sticking to the mdmAppId-attribute when there’s also an IsManaged-attribute. I have my reasons.

First of all, Autopilot-devices don’t set their IsManaged-attribute to true until they actually perform their MDM-enrollment. For these devices, the mdmAppId will tell us if it already is or will be Intune-managed. Second, I not only wanted to know if the device is managed, but also how that’s done.

And, finally, because that’s how the portal told me to do it 😊.

One Comment

Let me know what you think!

This site uses Akismet to reduce spam. Learn how your comment data is processed.